diff --git a/bun.lock b/bun.lock index e316834a099..6e754781d47 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "activepieces", @@ -1831,7 +1830,7 @@ }, "packages/pieces/community/customer-io": { "name": "@activepieces/piece-customer-io", - "version": "0.3.4", + "version": "0.3.5", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -2246,7 +2245,7 @@ }, "packages/pieces/community/exa": { "name": "@activepieces/piece-exa", - "version": "0.1.4", + "version": "0.1.5", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -2851,7 +2850,7 @@ }, "packages/pieces/community/google-search-console": { "name": "@activepieces/piece-google-search-console", - "version": "0.1.4", + "version": "0.1.5", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -2901,6 +2900,17 @@ "tslib": "2.6.2", }, }, + "packages/pieces/community/google-vertexai": { + "name": "@activepieces/piece-google-vertexai", + "version": "0.0.1", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "@google/genai": "1.43.0", + "google-auth-library": "10.6.1", + }, + }, "packages/pieces/community/googlechat": { "name": "@activepieces/piece-googlechat", "version": "0.1.3", @@ -3721,7 +3731,7 @@ }, "packages/pieces/community/logsnag": { "name": "@activepieces/piece-logsnag", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4033,7 +4043,7 @@ }, "packages/pieces/community/microsoft-365-people": { "name": "@activepieces/piece-microsoft-365-people", - "version": "0.1.9", + "version": "0.1.10", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4046,7 +4056,7 @@ }, "packages/pieces/community/microsoft-365-planner": { "name": "@activepieces/piece-microsoft-365-planner", - "version": "0.1.9", + "version": "0.1.10", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4059,7 +4069,7 @@ }, "packages/pieces/community/microsoft-dynamics-365-business-central": { "name": "@activepieces/piece-microsoft-dynamics-365-business-central", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4070,7 +4080,7 @@ }, "packages/pieces/community/microsoft-dynamics-crm": { "name": "@activepieces/piece-microsoft-dynamics-crm", - "version": "0.2.5", + "version": "0.2.6", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4080,7 +4090,7 @@ }, "packages/pieces/community/microsoft-excel-365": { "name": "@activepieces/piece-microsoft-excel-365", - "version": "0.4.12", + "version": "0.4.13", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4093,7 +4103,7 @@ }, "packages/pieces/community/microsoft-onedrive": { "name": "@activepieces/piece-microsoft-onedrive", - "version": "0.1.9", + "version": "0.1.10", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4110,7 +4120,7 @@ }, "packages/pieces/community/microsoft-onenote": { "name": "@activepieces/piece-microsoft-onenote", - "version": "0.1.8", + "version": "0.1.9", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4122,7 +4132,7 @@ }, "packages/pieces/community/microsoft-outlook": { "name": "@activepieces/piece-microsoft-outlook", - "version": "0.2.12", + "version": "0.2.13", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4135,7 +4145,7 @@ }, "packages/pieces/community/microsoft-outlook-calendar": { "name": "@activepieces/piece-microsoft-outlook-calendar", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4146,7 +4156,7 @@ }, "packages/pieces/community/microsoft-power-bi": { "name": "@activepieces/piece-microsoft-power-bi", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4156,7 +4166,7 @@ }, "packages/pieces/community/microsoft-sharepoint": { "name": "@activepieces/piece-microsoft-sharepoint", - "version": "0.2.11", + "version": "0.2.12", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4169,7 +4179,7 @@ }, "packages/pieces/community/microsoft-teams": { "name": "@activepieces/piece-microsoft-teams", - "version": "0.3.11", + "version": "0.3.12", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4182,7 +4192,7 @@ }, "packages/pieces/community/microsoft-todo": { "name": "@activepieces/piece-microsoft-todo", - "version": "0.2.9", + "version": "0.2.10", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4375,7 +4385,7 @@ }, "packages/pieces/community/mycase-piece": { "name": "@activepieces/piece-mycase-piece", - "version": "0.1.3", + "version": "0.1.4", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4385,7 +4395,7 @@ }, "packages/pieces/community/mysendingbox": { "name": "@activepieces/piece-mysendingbox", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4649,7 +4659,7 @@ }, "packages/pieces/community/opnform": { "name": "@activepieces/piece-opnform", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4781,7 +4791,7 @@ }, "packages/pieces/community/pdf": { "name": "@activepieces/piece-pdf", - "version": "0.3.3", + "version": "0.3.4", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -5418,7 +5428,7 @@ }, "packages/pieces/community/salesforce": { "name": "@activepieces/piece-salesforce", - "version": "0.6.0", + "version": "0.6.1", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -5639,7 +5649,7 @@ }, "packages/pieces/community/sign-now": { "name": "@activepieces/piece-sign-now", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -5722,7 +5732,7 @@ }, "packages/pieces/community/slack": { "name": "@activepieces/piece-slack", - "version": "0.12.4", + "version": "0.12.5", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -7514,7 +7524,7 @@ }, "packages/shared": { "name": "@activepieces/shared", - "version": "0.40.1", + "version": "0.41.0", "dependencies": { "@sinclair/typebox": "0.34.11", "deepmerge-ts": "7.1.0", @@ -8163,6 +8173,8 @@ "@activepieces/piece-google-tasks": ["@activepieces/piece-google-tasks@workspace:packages/pieces/community/google-tasks"], + "@activepieces/piece-google-vertexai": ["@activepieces/piece-google-vertexai@workspace:packages/pieces/community/google-vertexai"], + "@activepieces/piece-googlechat": ["@activepieces/piece-googlechat@workspace:packages/pieces/community/googlechat"], "@activepieces/piece-gotify": ["@activepieces/piece-gotify@workspace:packages/pieces/community/gotify"], @@ -14467,6 +14479,10 @@ "@activepieces/piece-drupal/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@activepieces/piece-google-vertexai/@google/genai": ["@google/genai@1.43.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hklCsJNdMlDM1IwcCVcGQFBg2izY0+t5BIGbRsxi2UnKi6AGKL7pqJqmBDNRbw0bYCs4y3NA7TB+fkKfP/Nrdw=="], + + "@activepieces/piece-google-vertexai/google-auth-library": ["google-auth-library@10.6.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "7.1.3", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA=="], + "@activepieces/piece-prompthub/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@activepieces/piece-rabbitmq/@types/amqplib": ["@types/amqplib@0.10.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA=="], @@ -16831,6 +16847,14 @@ "@activepieces/piece-amazon-secrets-manager/@aws-sdk/client-secrets-manager/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@activepieces/piece-google-vertexai/@google/genai/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + + "@activepieces/piece-google-vertexai/google-auth-library/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "@activepieces/piece-google-vertexai/google-auth-library/gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "@activepieces/piece-google-vertexai/google-auth-library/jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "@activepieces/piece-rabbitmq/@types/amqplib/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], "@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -18023,6 +18047,14 @@ "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "@activepieces/piece-google-vertexai/@google/genai/protobufjs/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], + + "@activepieces/piece-google-vertexai/@google/genai/protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "@activepieces/piece-google-vertexai/google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "@activepieces/piece-google-vertexai/google-auth-library/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + "@activepieces/piece-rabbitmq/@types/amqplib/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], "@ai-sdk/google-vertex/google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -18461,6 +18493,8 @@ "vite-plugin-checker/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@activepieces/piece-google-vertexai/@google/genai/protobufjs/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "@atlaskit/editor-json-transformer/@atlaskit/editor-prosemirror/prosemirror-commands/prosemirror-state/prosemirror-view": ["prosemirror-view@1.41.6", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg=="], "@atlaskit/editor-json-transformer/@atlaskit/editor-prosemirror/prosemirror-dropcursor/prosemirror-state/prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="], diff --git a/docker-compose.yml b/docker-compose.yml index 283e4071207..3970695ee88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,3 +41,4 @@ volumes: redis_data: networks: activepieces: + diff --git a/package.json b/package.json index 51fc70c3de6..0a56205cc2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "activepieces", - "version": "0.79.0", + "version": "0.79.1", "rcVersion": "0.78.0-rc.0", "packageManager": "bun@1.3.3", "scripts": { diff --git a/packages/pieces/community/ai/src/lib/actions/agents/tools.ts b/packages/pieces/community/ai/src/lib/actions/agents/tools.ts index 657e62ad224..f3313c60438 100644 --- a/packages/pieces/community/ai/src/lib/actions/agents/tools.ts +++ b/packages/pieces/community/ai/src/lib/actions/agents/tools.ts @@ -2,7 +2,7 @@ import { dynamicTool, LanguageModel, Tool } from "ai"; import z from "zod"; import { agentUtils } from "./utils"; import { agentOutputBuilder } from "./agent-output-builder"; -import { AgentMcpTool, AgentOutputField, AgentTaskStatus, AgentTool, AgentToolType, buildAuthHeaders, isNil, isString, McpProtocol, sanitizeToolName, TASK_COMPLETION_TOOL_NAME } from "@activepieces/shared"; +import { AgentMcpTool, AgentOutputField, AgentTaskStatus, AgentTool, AgentToolType, buildAuthHeaders, isNil, isString, McpProtocol, createToolName, TASK_COMPLETION_TOOL_NAME } from "@activepieces/shared"; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { ActionContext } from "@activepieces/pieces-framework"; import { experimental_createMCPClient as createMCPClient, MCPClient, MCPTransport } from '@ai-sdk/mcp'; @@ -60,7 +60,7 @@ function flattenMcpServers( const agentTool = agentMcpTools.find((t) => t.toolName === server.mcpName); for (const [toolName, fn] of Object.entries(server.tools)) { - const key = sanitizeToolName(`${toolName}`); + const key = createToolName(`${toolName}`); tools[key] = fn; if (agentTool) { keyToAgentTool[key] = agentTool; @@ -131,7 +131,7 @@ export async function constructAgentTools( const toolKeyToAgentTool: Record = {}; for (const agentTool of agentTools.filter(t => t.type !== AgentToolType.MCP)) { - const key = agentTool.type === AgentToolType.FLOW ? sanitizeToolName(agentTool.toolName) : agentTool.toolName; + const key = agentTool.type === AgentToolType.FLOW ? createToolName(agentTool.toolName) : agentTool.toolName; toolKeyToAgentTool[key] = agentTool; } Object.assign(toolKeyToAgentTool, mcpKeyToAgentTool); diff --git a/packages/pieces/community/ai/src/lib/actions/agents/utils.ts b/packages/pieces/community/ai/src/lib/actions/agents/utils.ts index 82c85fd9315..a8012a27e92 100644 --- a/packages/pieces/community/ai/src/lib/actions/agents/utils.ts +++ b/packages/pieces/community/ai/src/lib/actions/agents/utils.ts @@ -11,7 +11,7 @@ import { McpProperty, McpPropertyType, AgentFlowTool, - sanitizeToolName, + createToolName, } from '@activepieces/shared'; import { z, ZodObject } from 'zod'; import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common'; @@ -108,7 +108,7 @@ export const agentUtils = { mcpPropertyToSchema(prop), ]), ) - const sanitizedName = sanitizeToolName(tool.toolName) + const sanitizedName = createToolName(tool.toolName) return { name: sanitizedName, description: toolDescription, diff --git a/packages/pieces/community/google-vertexai/.eslintrc.json b/packages/pieces/community/google-vertexai/.eslintrc.json new file mode 100644 index 00000000000..eac708a1fa9 --- /dev/null +++ b/packages/pieces/community/google-vertexai/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/pieces/community/google-vertexai/README.md b/packages/pieces/community/google-vertexai/README.md new file mode 100644 index 00000000000..ea3873b207c --- /dev/null +++ b/packages/pieces/community/google-vertexai/README.md @@ -0,0 +1,142 @@ +# Google Vertex AI Piece + +Integrate your automation workflows with Google Vertex AI to leverage powerful AI models including Gemini and other advanced language models. + +## Overview + +The Google Vertex AI piece enables you to: +- Generate content and responses using Gemini and other Vertex AI models +- Make custom API requests to Vertex AI endpoints +- Build intelligent automation workflows with AI capabilities + +## Authentication + +This piece uses **Service Account Authentication** for secure access to Google Vertex AI APIs. + +### How to Set Up + +1. **Create a Google Cloud Project** + - Go to [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project or select an existing one + +2. **Enable Vertex AI API** + - Navigate to "APIs & Services" > "Enabled APIs & services" + - Click "Enable APIs and Services" + - Search for "Vertex AI API" + - Click on it and press "Enable" + +3. **Create a Service Account** + - Go to "APIs & Services" > "Credentials" + - Click "Create Credentials" > "Service Account" + - Give it a name (e.g., "activepieces-vertexai") + - Click "Create and Continue" + - Grant the following role: + - **Vertex AI User** - to use Vertex AI models + - Click "Continue" and then "Done" + +4. **Create and Download Service Account Key** + - Click on the newly created service account + - Go to the "Keys" tab + - Click "Add Key" > "Create new key" + - Choose **JSON** format + - Click "Create" - this will download a JSON file + +5. **Add the Key to Activepieces** + - In Activepieces, paste the entire JSON content from the downloaded file into the "Service Account JSON Key" field + - The piece will validate the credentials automatically + +## Actions + +### 1. Generate Content (Gemini) + +Generate responses using Google Vertex AI's Gemini model. + +**Parameters:** +- **Location** (default: `us-central1`) - Google Cloud region where your Vertex AI resources are located +- **Model** (default: `gemini-2.5-flash`) - The Vertex AI model to use (e.g., `gemini-pro`, `gemini-2.5-flash`) +- **System Message** (optional) - Instructions to guide the model's behavior and tone +- **User Message** (required) - The prompt or question to send to the model + +**Example Output:** +```json +{ + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "The model's response here..." + } + ] + } + } + ] +} +``` + +### 2. Raw API Request + +Make custom authenticated requests directly to the Vertex AI API for advanced use cases. + +**Parameters:** +- **Method** (required) - HTTP method: GET, POST, PUT, PATCH, or DELETE +- **URL** (required) - Full Vertex AI API endpoint URL (e.g., `https://aiplatform.googleapis.com/v1/projects/...`) +- **Body** (optional) - JSON payload for the request + +**Example:** +- Method: `POST` +- URL: `https://aiplatform.googleapis.com/v1/projects/{projectId}/locations/us-central1/publishers/google/models/gemini-2.5-flash:generateContent` +- Body: Custom JSON payload + +## Available Models + +Common Gemini models available on Vertex AI: +- `gemini-2.5-flash` - Fast, cost-efficient model with thinking support +- `gemini-2.5-pro` - Most capable model for complex reasoning tasks +- `gemini-2.0-flash` - Balanced speed and capability +- `gemini-1.5-flash` - Fast and versatile +- `gemini-1.5-pro` - Best performing 1.5 generation model + +For the full list of available models, check the [Vertex AI documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models). + +## Common Use Cases + +### 1. Generate Blog Content +Use the Generate Content action with a detailed prompt to automatically create blog posts based on a topic. + +### 2. Customer Support Automation +Provide customer queries to Gemini for intelligent response suggestions. + +### 3. Code Generation +Use the Generate Content action with code-related prompts to generate code snippets. + +### 4. Data Analysis +Send data to Vertex AI for analysis and insights. + +### 5. Custom API Integration +Use the Raw API Request action for advanced Vertex AI features not covered by the basic actions. + +## Error Handling + +Common errors and solutions: + +- **Authentication Failed**: Verify your Service Account JSON key is valid and has the necessary permissions +- **API Not Enabled**: Ensure Vertex AI API is enabled in your Google Cloud project +- **Invalid Model**: Check the model name matches available Vertex AI models in your region +- **Location Not Found**: Verify the specified location is available in your Google Cloud region + +## Best Practices + +1. **Use System Messages**: Provide clear system messages to guide the model's behavior +2. **Handle Rate Limits**: Implement delays if making many requests in succession +3. **Cache Responses**: Store responses when possible to reduce API calls +4. **Monitor Costs**: Vertex AI API calls incur costs - monitor your usage in Google Cloud Console +5. **Use Raw Request for Advanced Features**: When specific features aren't available through the basic actions, use the Raw API Request action + +## Links + +- [Vertex AI Documentation](https://cloud.google.com/vertex-ai/docs) +- [Vertex AI API Reference](https://cloud.google.com/vertex-ai/docs/reference/rest) +- [Gemini Model Documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models) +- [Google Cloud Console](https://console.cloud.google.com/) diff --git a/packages/pieces/community/google-vertexai/package.json b/packages/pieces/community/google-vertexai/package.json new file mode 100644 index 00000000000..f8189f32b41 --- /dev/null +++ b/packages/pieces/community/google-vertexai/package.json @@ -0,0 +1,17 @@ +{ + "name": "@activepieces/piece-google-vertexai", + "version": "0.0.1", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "@google/genai": "1.43.0", + "google-auth-library": "10.6.1" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + } +} diff --git a/packages/pieces/community/google-vertexai/src/i18n/translation.json b/packages/pieces/community/google-vertexai/src/i18n/translation.json new file mode 100644 index 00000000000..74adb27ef14 --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/i18n/translation.json @@ -0,0 +1,4 @@ +{ + "Action": {}, + "Trigger": {} +} \ No newline at end of file diff --git a/packages/pieces/community/google-vertexai/src/index.ts b/packages/pieces/community/google-vertexai/src/index.ts new file mode 100644 index 00000000000..4eb167e8894 --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/index.ts @@ -0,0 +1,18 @@ +import { createPiece } from "@activepieces/pieces-framework"; +import { PieceCategory } from "@activepieces/shared"; +import { vertexAiAuth } from "./lib/auth"; +import { generateContent, generateImage, customApiCall } from "./lib/actions"; + +export const googleVertexai = createPiece({ + displayName: "Google Vertex AI", + description: "Generate content and images using Gemini and Imagen models on Google Vertex AI.", + auth: vertexAiAuth, + minimumSupportedRelease: "0.71.4", + logoUrl: "https://cdn.activepieces.com/pieces/google-vertexai.png", + categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE], + authors: ["alinperghel", "onyedikachi-david"], + actions: [generateContent, generateImage, customApiCall], + triggers: [], +}); + +export { vertexAiAuth, GoogleVertexAIAuthValue } from "./lib/auth"; diff --git a/packages/pieces/community/google-vertexai/src/lib/actions/generate-content.ts b/packages/pieces/community/google-vertexai/src/lib/actions/generate-content.ts new file mode 100644 index 00000000000..27df1af0b0c --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/lib/actions/generate-content.ts @@ -0,0 +1,188 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { + GenerateContentConfig, + GoogleGenAI, + Part, + ThinkingConfig, + ThinkingLevel, +} from '@google/genai'; +import { vertexAiAuth, GoogleVertexAIAuthValue } from '../auth'; +import { getVertexAIModelOptions, getVertexAILocationOptions } from '../common'; + +export const generateContent = createAction({ + auth: vertexAiAuth, + name: 'generate_content', + displayName: 'Generate Content', + description: 'Call a Gemini model on Vertex AI to generate a text response.', + props: { + location: Property.Dropdown({ + displayName: 'Location', + description: 'Google Cloud region where your Vertex AI resources are hosted.', + required: true, + refreshers: [], + defaultValue: 'us-central1', + auth: vertexAiAuth, + options: async ({ auth }) => + getVertexAILocationOptions(auth as GoogleVertexAIAuthValue | undefined), + }), + model: Property.Dropdown({ + displayName: 'Model', + description: 'Gemini model to use for content generation.', + required: true, + refreshers: ['location'], + defaultValue: 'gemini-2.5-flash', + auth: vertexAiAuth, + options: async ({ auth, location }) => + getVertexAIModelOptions( + auth as GoogleVertexAIAuthValue | undefined, + location as string | undefined + ), + }), + systemMessage: Property.LongText({ + displayName: 'System Message', + description: "Instructions that guide the model's behavior throughout the conversation.", + required: false, + }), + userMessage: Property.LongText({ + displayName: 'User Message', + description: 'The prompt to send to the model.', + required: true, + }), + imageUrls: Property.Array({ + displayName: 'Image URLs', + description: + 'Public image URLs to include alongside the user message. The model will analyze these images.', + required: false, + }), + temperature: Property.Number({ + displayName: 'Temperature', + description: + 'Controls randomness. Lower values are more deterministic (0–2). Leave empty to use the model default.', + required: false, + }), + maxOutputTokens: Property.Number({ + displayName: 'Max Output Tokens', + description: 'Maximum number of tokens to generate. Leave empty to use the model default.', + required: false, + }), + thinkingLevel: Property.StaticDropdown({ + displayName: 'Thinking Level', + description: + 'Controls how much the model thinks before responding. Supported on Gemini 2.5 and later models.', + required: false, + options: { + options: [ + { label: 'Low', value: ThinkingLevel.LOW }, + { label: 'Medium', value: ThinkingLevel.MEDIUM }, + { label: 'High', value: ThinkingLevel.HIGH }, + ], + }, + }), + thinkingBudget: Property.Number({ + displayName: 'Thinking Budget (tokens)', + description: + 'Maximum number of tokens the model can use for internal reasoning. Set to 0 to disable thinking. Supported on Gemini 2.5 and later models.', + required: false, + }), + includeThoughts: Property.Checkbox({ + displayName: 'Include Thoughts in Response', + description: + "When enabled, the model's reasoning is returned alongside the final response. Supported on Gemini 2.5 and later models.", + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { + location, + model, + systemMessage, + userMessage, + imageUrls, + temperature, + maxOutputTokens, + thinkingLevel, + thinkingBudget, + includeThoughts, + } = context.propsValue; + const auth = context.auth as GoogleVertexAIAuthValue; + + const rawCredentials = JSON.parse(auth.props.serviceAccountJson); + const credentials = { + ...rawCredentials, + private_key: rawCredentials.private_key?.replace(/\\n/g, '\n'), + }; + + const ai = new GoogleGenAI({ + vertexai: true, + project: credentials.project_id, + location: location, + googleAuthOptions: { + credentials, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + }); + + const parts: Part[] = [{ text: userMessage }]; + + for (const imageUrl of (imageUrls as string[] | undefined) ?? []) { + try { + const imageResponse = await fetch(imageUrl); + if (!imageResponse.ok) { + throw new Error(`HTTP ${imageResponse.status} ${imageResponse.statusText}`); + } + const arrayBuffer = await imageResponse.arrayBuffer(); + const base64 = Buffer.from(arrayBuffer).toString('base64'); + const mimeType = imageResponse.headers.get('content-type') ?? 'image/jpeg'; + parts.push({ inlineData: { data: base64, mimeType } }); + } catch (err) { + throw new Error( + `Failed to fetch image (${imageUrl}): ${err instanceof Error ? err.message : String(err)}` + ); + } + } + + const config: GenerateContentConfig = {}; + + if (systemMessage) { + config.systemInstruction = systemMessage; + } + if (temperature !== undefined && temperature !== null) { + config.temperature = temperature; + } + if (maxOutputTokens !== undefined && maxOutputTokens !== null) { + config.maxOutputTokens = maxOutputTokens; + } + + const thinkingConfig: ThinkingConfig = {}; + let hasThinkingConfig = false; + + if (thinkingLevel) { + thinkingConfig.thinkingLevel = thinkingLevel as ThinkingLevel; + hasThinkingConfig = true; + } + if (thinkingBudget !== undefined && thinkingBudget !== null) { + thinkingConfig.thinkingBudget = thinkingBudget; + hasThinkingConfig = true; + } + if (includeThoughts) { + thinkingConfig.includeThoughts = true; + hasThinkingConfig = true; + } + if (hasThinkingConfig) { + config.thinkingConfig = thinkingConfig; + } + + const response = await ai.models.generateContent({ + model, + contents: [{ role: 'user', parts }], + config, + }); + + return { + text: response.text, + candidates: response.candidates, + usageMetadata: response.usageMetadata, + }; + }, +}); diff --git a/packages/pieces/community/google-vertexai/src/lib/actions/generate-image.ts b/packages/pieces/community/google-vertexai/src/lib/actions/generate-image.ts new file mode 100644 index 00000000000..e4c397c242a --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/lib/actions/generate-image.ts @@ -0,0 +1,179 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { GoogleGenAI } from '@google/genai'; +import { vertexAiAuth, GoogleVertexAIAuthValue } from '../auth'; +import { getVertexAILocationOptions, getVertexAIImageModelOptions } from '../common'; + +export const generateImage = createAction({ + auth: vertexAiAuth, + name: 'generate_image', + displayName: 'Generate Image', + description: + 'Generate an image from a text prompt using Google Imagen models on Vertex AI.', + props: { + location: Property.Dropdown({ + displayName: 'Location', + description: 'Google Cloud region where your Vertex AI resources are hosted.', + required: true, + refreshers: [], + defaultValue: 'us-central1', + auth: vertexAiAuth, + options: async ({ auth }) => + getVertexAILocationOptions(auth as GoogleVertexAIAuthValue | undefined), + }), + model: Property.Dropdown({ + displayName: 'Model', + description: 'Imagen model to use for image generation.', + required: true, + refreshers: ['location'], + defaultValue: 'imagen-4.0-generate-001', + auth: vertexAiAuth, + options: async ({ auth, location }) => + getVertexAIImageModelOptions( + auth as GoogleVertexAIAuthValue | undefined, + location as string | undefined + ), + }), + prompt: Property.LongText({ + displayName: 'Prompt', + description: 'A text description of the image you want to generate.', + required: true, + }), + negativePrompt: Property.LongText({ + displayName: 'Negative Prompt', + description: 'Describe what you do NOT want in the image.', + required: false, + }), + numberOfImages: Property.Number({ + displayName: 'Number of Images', + description: 'Number of images to generate (1–4).', + required: false, + defaultValue: 1, + }), + aspectRatio: Property.StaticDropdown({ + displayName: 'Aspect Ratio', + description: 'Aspect ratio of the generated image.', + required: false, + defaultValue: '1:1', + options: { + options: [ + { label: 'Square (1:1)', value: '1:1' }, + { label: 'Portrait (3:4)', value: '3:4' }, + { label: 'Landscape (4:3)', value: '4:3' }, + { label: 'Portrait (9:16)', value: '9:16' }, + { label: 'Landscape (16:9)', value: '16:9' }, + ], + }, + }), + outputMimeType: Property.StaticDropdown({ + displayName: 'Output Format', + description: 'Image format for the generated output.', + required: false, + defaultValue: 'image/png', + options: { + options: [ + { label: 'PNG', value: 'image/png' }, + { label: 'JPEG', value: 'image/jpeg' }, + ], + }, + }), + seed: Property.Number({ + displayName: 'Seed', + description: + 'Random seed for reproducible results. Requires "Add Watermark" to be disabled.', + required: false, + }), + enhancePrompt: Property.Checkbox({ + displayName: 'Enhance Prompt', + description: + 'When enabled, the model rewrites your prompt to improve output quality.', + required: false, + defaultValue: false, + }), + addWatermark: Property.Checkbox({ + displayName: 'Add Watermark', + description: 'When enabled, adds an invisible watermark to generated images.', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { + location, + model, + prompt, + negativePrompt, + numberOfImages, + aspectRatio, + outputMimeType, + seed, + enhancePrompt, + addWatermark, + } = context.propsValue; + const auth = context.auth as GoogleVertexAIAuthValue; + + const rawCredentials = JSON.parse(auth.props.serviceAccountJson); + const credentials = { + ...rawCredentials, + private_key: rawCredentials.private_key?.replace(/\\n/g, '\n'), + }; + + const ai = new GoogleGenAI({ + vertexai: true, + project: credentials.project_id, + location, + googleAuthOptions: { + credentials, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + }); + + const response = await ai.models.generateImages({ + model, + prompt, + config: { + ...(negativePrompt ? { negativePrompt } : {}), + numberOfImages: numberOfImages ?? 1, + aspectRatio: aspectRatio ?? '1:1', + outputMimeType: outputMimeType ?? 'image/png', + ...(seed !== undefined && seed !== null ? { seed } : {}), + enhancePrompt: enhancePrompt ?? false, + addWatermark: addWatermark ?? false, + }, + }); + + const generated = response.generatedImages ?? []; + + if (generated.length === 0) { + throw new Error( + 'No images were generated. The prompt may have been blocked by safety filters.' + ); + } + + const ext = (outputMimeType ?? 'image/png') === 'image/jpeg' ? 'jpg' : 'png'; + + const images = await Promise.all( + generated.map(async (img, index) => { + const imageBytes = img.image?.imageBytes; + if (!imageBytes) { + throw new Error( + img.raiFilteredReason + ? `Image ${index + 1} was filtered: ${img.raiFilteredReason}` + : `Image ${index + 1} did not return any data.` + ); + } + + const file = await context.files.write({ + fileName: `generated-image-${index + 1}.${ext}`, + data: Buffer.from(imageBytes, 'base64'), + }); + + return { + image: file, + ...(img.enhancedPrompt ? { enhancedPrompt: img.enhancedPrompt } : {}), + }; + }) + ); + + return { images, model, prompt }; + }, +}); diff --git a/packages/pieces/community/google-vertexai/src/lib/actions/index.ts b/packages/pieces/community/google-vertexai/src/lib/actions/index.ts new file mode 100644 index 00000000000..83927eb881e --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/lib/actions/index.ts @@ -0,0 +1,3 @@ +export { generateContent } from './generate-content'; +export { generateImage } from './generate-image'; +export { customApiCall } from './raw-request'; diff --git a/packages/pieces/community/google-vertexai/src/lib/actions/raw-request.ts b/packages/pieces/community/google-vertexai/src/lib/actions/raw-request.ts new file mode 100644 index 00000000000..f193b5f83a1 --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/lib/actions/raw-request.ts @@ -0,0 +1,31 @@ +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { GoogleAuth } from 'google-auth-library'; +import { vertexAiAuth, GoogleVertexAIAuthValue } from '../auth'; + +function parseRawCredentials(auth: GoogleVertexAIAuthValue) { + const raw = JSON.parse(auth.props.serviceAccountJson); + return { ...raw, private_key: raw.private_key?.replace(/\\n/g, '\n') }; +} + +export const customApiCall = createCustomApiCallAction({ + auth: vertexAiAuth, + baseUrl: (auth) => { + try { + const credentials = parseRawCredentials(auth as GoogleVertexAIAuthValue); + return `https://aiplatform.googleapis.com/v1/projects/${credentials.project_id}`; + } catch { + return 'https://aiplatform.googleapis.com/v1'; + } + }, + authMapping: async (auth) => { + const credentials = parseRawCredentials(auth as GoogleVertexAIAuthValue); + const googleAuth = new GoogleAuth({ + credentials, + projectId: credentials.project_id, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + const accessToken = await googleAuth.getAccessToken(); + if (!accessToken) throw new Error('Failed to obtain access token'); + return { Authorization: `Bearer ${accessToken}` }; + }, +}); diff --git a/packages/pieces/community/google-vertexai/src/lib/auth.ts b/packages/pieces/community/google-vertexai/src/lib/auth.ts new file mode 100644 index 00000000000..3d6dbb2ce04 --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/lib/auth.ts @@ -0,0 +1,64 @@ +import { AppConnectionValueForAuthProperty, PieceAuth, Property } from '@activepieces/pieces-framework'; + +export const vertexAiAuth = PieceAuth.CustomAuth({ + description: ` + To authenticate with Google Vertex AI using a Service Account: + 1. Go to [Google Cloud Console](https://console.cloud.google.com). + 2. Create a new project or select an existing one. + 3. Enable the **Vertex AI API** under *APIs & Services > Enabled APIs*. + 4. Create a Service Account: + - Go to **IAM & Admin > Service Accounts**. + - Click **Create Service Account**, fill in the details and click **Create**. + - Grant the **Vertex AI User** role. + 5. Create a JSON key: + - Click on the created service account. + - Go to the **Keys** tab. + - Click **Add Key > Create new key**, choose **JSON** and click **Create**. + - The JSON key file will be downloaded automatically. + 6. Copy the **entire contents** of the downloaded JSON file and paste it below. + `, + required: true, + props: { + serviceAccountJson: Property.LongText({ + displayName: 'Service Account JSON', + description: 'The complete JSON content from your Google Service Account key file.', + required: true, + }), + }, + validate: async ({ auth }) => { + try { + const credentials = JSON.parse(auth.serviceAccountJson); + + if (!credentials.type || credentials.type !== 'service_account') { + return { valid: false, error: 'Invalid service account JSON: missing or incorrect "type" field.' }; + } + if (!credentials.project_id) { + return { valid: false, error: 'Invalid service account JSON: missing "project_id" field.' }; + } + if (!credentials.private_key) { + return { valid: false, error: 'Invalid service account JSON: missing "private_key" field.' }; + } + if (!credentials.client_email) { + return { valid: false, error: 'Invalid service account JSON: missing "client_email" field.' }; + } + + const { JWT } = await import('google-auth-library'); + const jwtClient = new JWT({ + email: credentials.client_email, + key: credentials.private_key.replace(/\\n/g, '\n'), + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + + await jwtClient.authorize(); + + return { valid: true }; + } catch (e) { + if (e instanceof SyntaxError) { + return { valid: false, error: 'Invalid JSON format. Please copy the complete service account JSON file.' }; + } + return { valid: false, error: `Authentication failed: ${e instanceof Error ? e.message : 'Unknown error'}` }; + } + }, +}); + +export type GoogleVertexAIAuthValue = AppConnectionValueForAuthProperty; diff --git a/packages/pieces/community/google-vertexai/src/lib/common.ts b/packages/pieces/community/google-vertexai/src/lib/common.ts new file mode 100644 index 00000000000..cae21c93d3c --- /dev/null +++ b/packages/pieces/community/google-vertexai/src/lib/common.ts @@ -0,0 +1,161 @@ +import { GoogleGenAI } from '@google/genai'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { GoogleAuth } from 'google-auth-library'; +import { GoogleVertexAIAuthValue } from './auth'; + +const VERTEX_AI_LOCATIONS = [ + { label: 'us-central1 (Iowa)', value: 'us-central1' }, + { label: 'us-east1 (South Carolina)', value: 'us-east1' }, + { label: 'us-east4 (Northern Virginia)', value: 'us-east4' }, + { label: 'us-east5 (Columbus)', value: 'us-east5' }, + { label: 'us-south1 (Dallas)', value: 'us-south1' }, + { label: 'us-west1 (Oregon)', value: 'us-west1' }, + { label: 'us-west4 (Las Vegas)', value: 'us-west4' }, + { label: 'northamerica-northeast1 (Montréal)', value: 'northamerica-northeast1' }, + { label: 'northamerica-northeast2 (Toronto)', value: 'northamerica-northeast2' }, + { label: 'europe-central2 (Warsaw)', value: 'europe-central2' }, + { label: 'europe-north1 (Finland)', value: 'europe-north1' }, + { label: 'europe-southwest1 (Madrid)', value: 'europe-southwest1' }, + { label: 'europe-west1 (Belgium)', value: 'europe-west1' }, + { label: 'europe-west2 (London)', value: 'europe-west2' }, + { label: 'europe-west3 (Frankfurt)', value: 'europe-west3' }, + { label: 'europe-west4 (Netherlands)', value: 'europe-west4' }, + { label: 'europe-west6 (Zürich)', value: 'europe-west6' }, + { label: 'europe-west8 (Milan)', value: 'europe-west8' }, + { label: 'europe-west9 (Paris)', value: 'europe-west9' }, + { label: 'europe-west12 (Turin)', value: 'europe-west12' }, + { label: 'me-central1 (Doha)', value: 'me-central1' }, + { label: 'me-central2 (Dammam)', value: 'me-central2' }, + { label: 'me-west1 (Tel Aviv)', value: 'me-west1' }, + { label: 'africa-south1 (Johannesburg)', value: 'africa-south1' }, + { label: 'asia-east1 (Taiwan)', value: 'asia-east1' }, + { label: 'asia-east2 (Hong Kong)', value: 'asia-east2' }, + { label: 'asia-northeast1 (Tokyo)', value: 'asia-northeast1' }, + { label: 'asia-northeast2 (Osaka)', value: 'asia-northeast2' }, + { label: 'asia-northeast3 (Seoul)', value: 'asia-northeast3' }, + { label: 'asia-south1 (Mumbai)', value: 'asia-south1' }, + { label: 'asia-south2 (Delhi)', value: 'asia-south2' }, + { label: 'asia-southeast1 (Singapore)', value: 'asia-southeast1' }, + { label: 'asia-southeast2 (Jakarta)', value: 'asia-southeast2' }, + { label: 'australia-southeast1 (Sydney)', value: 'australia-southeast1' }, + { label: 'australia-southeast2 (Melbourne)', value: 'australia-southeast2' }, +]; + +function parseCredentials(auth: GoogleVertexAIAuthValue) { + const raw = JSON.parse(auth.props.serviceAccountJson); + return { + ...raw, + private_key: raw.private_key?.replace(/\\n/g, '\n'), + }; +} + +function buildGenAIClient(credentials: Record, location: string) { + return new GoogleGenAI({ + vertexai: true, + project: credentials['project_id'], + location, + googleAuthOptions: { + credentials, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }, + }); +} + +async function fetchVertexAIModels( + auth: GoogleVertexAIAuthValue, + location: string, + nameFilter: string +): Promise<{ label: string; value: string }[]> { + const credentials = parseCredentials(auth); + const ai = buildGenAIClient(credentials, location); + + const options: { label: string; value: string }[] = []; + const pager = await ai.models.list({ config: { queryBase: true } }); + + for await (const model of pager) { + if (!model.name?.toLowerCase().includes(nameFilter)) continue; + const modelId = model.name.replace(/^(publishers\/google\/models\/|models\/)/, ''); + options.push({ label: model.displayName ?? modelId, value: modelId }); + } + + return options; +} + +interface VertexLocation { + locationId: string; +} + +export async function getVertexAILocationOptions(auth: GoogleVertexAIAuthValue | undefined) { + if (!auth) { + return { disabled: true, placeholder: 'Connect your account first', options: [] }; + } + + try { + const credentials = parseCredentials(auth); + + const googleAuth = new GoogleAuth({ + credentials, + projectId: credentials['project_id'], + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + const accessToken = await googleAuth.getAccessToken(); + if (!accessToken) throw new Error('Failed to obtain access token'); + + const response = await httpClient.sendRequest<{ locations: VertexLocation[] }>({ + method: HttpMethod.GET, + url: `https://aiplatform.googleapis.com/v1/projects/${credentials['project_id']}/locations`, + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + const options = (response.body.locations ?? []).map((loc) => ({ + label: loc.locationId, + value: loc.locationId, + })); + + if (options.length > 0) { + return { disabled: false, options }; + } + } catch { + // fall through to static list + } + + return { disabled: false, options: VERTEX_AI_LOCATIONS }; +} + +export async function getVertexAIModelOptions( + auth: GoogleVertexAIAuthValue | undefined, + location: string | undefined +) { + if (!auth) { + return { disabled: true, placeholder: 'Connect your account first', options: [] }; + } + try { + const options = await fetchVertexAIModels(auth, location ?? 'us-central1', 'gemini'); + return { disabled: false, options }; + } catch { + return { + disabled: true, + options: [], + placeholder: "Couldn't load models. Check your credentials or location.", + }; + } +} + +export async function getVertexAIImageModelOptions( + auth: GoogleVertexAIAuthValue | undefined, + location: string | undefined +) { + if (!auth) { + return { disabled: true, placeholder: 'Connect your account first', options: [] }; + } + try { + const options = await fetchVertexAIModels(auth, location ?? 'us-central1', 'imagen'); + return { disabled: false, options }; + } catch { + return { + disabled: true, + options: [], + placeholder: "Couldn't load models. Check your credentials or location.", + }; + } +} diff --git a/packages/pieces/community/google-vertexai/tsconfig.json b/packages/pieces/community/google-vertexai/tsconfig.json new file mode 100644 index 00000000000..32bc5b18a43 --- /dev/null +++ b/packages/pieces/community/google-vertexai/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/google-vertexai/tsconfig.lib.json b/packages/pieces/community/google-vertexai/tsconfig.lib.json new file mode 100644 index 00000000000..1ae1ca7faf1 --- /dev/null +++ b/packages/pieces/community/google-vertexai/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/pdf/package.json b/packages/pieces/community/pdf/package.json index c7417bc06ee..08d88c4f8f3 100644 --- a/packages/pieces/community/pdf/package.json +++ b/packages/pieces/community/pdf/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-pdf", - "version": "0.3.3", + "version": "0.3.4", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/pdf/src/index.ts b/packages/pieces/community/pdf/src/index.ts index a3ee71de882..ae045ad0319 100644 --- a/packages/pieces/community/pdf/src/index.ts +++ b/packages/pieces/community/pdf/src/index.ts @@ -6,6 +6,7 @@ import { imageToPdf } from './lib/actions/image-to-pdf'; import { pdfPageCount } from './lib/actions/pdf-page-count'; import { extractPdfPages } from './lib/actions/extract-pdf-pages'; import { mergePdfs } from './lib/actions/merge-pdfs'; +import { addTextToPdf } from './lib/actions/add-text-to-pdf'; export const PDF = createPiece({ displayName: 'PDF', @@ -18,6 +19,7 @@ export const PDF = createPiece({ 'AbdulTheActivepiecer', 'jmgb27', 'danielpoonwj', + 'bertrandong', ], actions: [ extractText, @@ -27,6 +29,7 @@ export const PDF = createPiece({ pdfPageCount, extractPdfPages, mergePdfs, + addTextToPdf ], triggers: [], }); diff --git a/packages/pieces/community/pdf/src/lib/actions/add-text-to-pdf.ts b/packages/pieces/community/pdf/src/lib/actions/add-text-to-pdf.ts new file mode 100644 index 00000000000..1c070187a07 --- /dev/null +++ b/packages/pieces/community/pdf/src/lib/actions/add-text-to-pdf.ts @@ -0,0 +1,207 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { PDFDocument, rgb, StandardFonts, PDFFont } from 'pdf-lib'; + +const fontOptions = Object.entries(StandardFonts).map(([key, value]) => { + const formattedLabel = key.replace(/([A-Z])/g, ' $1').trim(); + return { label: formattedLabel, value: value }; +}); + +export const addTextToPdf = createAction({ + name: 'addTextToPdf', + displayName: 'Add Text to PDF', + description: 'Stamps one or more text strings at exact pixel distances from the top-left corner.', + props: { + file: Property.File({ + displayName: 'PDF File or URL', + required: true, + }), + textItems: Property.Array({ + displayName: 'Text Items to Insert', + description: 'Add each piece of text you want to stamp.', + required: true, + properties: { + text: Property.LongText({ + displayName: 'Text', + required: true, + }), + applyToAllPages: Property.Checkbox({ + displayName: 'Apply to all pages?', + description: 'If checked, this text is stamped on every page.', + required: false, + defaultValue: false, + }), + pageNumber: Property.Number({ + displayName: 'Page Number', + description: 'Which page to stamp? (Leave blank or ignore if applying to all pages)', + required: false, + defaultValue: 1, + }), + distanceFromLeft: Property.Number({ + displayName: 'Distance from Left Edge (in pixels)', + description: '0 is the far left edge of the page. Standard A4 width is about 595 pts.', + required: true, + }), + distanceFromTop: Property.Number({ + displayName: 'Distance from Top Edge (in pixels)', + description: '0 is the very top edge of the page. Standard A4 height is about 842 pts.', + required: true, + }), + font: Property.StaticDropdown({ + displayName: 'Font', + description: 'Select the exact font variant for this text item.', + required: true, + defaultValue: StandardFonts.Helvetica, + options: { + options: fontOptions, + }, + }), + fontSize: Property.Number({ + displayName: 'Font Size', + required: true, + defaultValue: 11, + }), + lineSpacing: Property.Number({ + displayName: 'Line Spacing', + description: 'The vertical spacing multiplier between lines. (Examples: 1.0 = Single, 1.15 = Standard, 2.0 = Double)', + required: false, + defaultValue: 1.15, + }), + }, + }), + }, + errorHandlingOptions: { + continueOnFailure: { + defaultValue: false, + }, + retryOnFailure: { + hide: true, + }, + }, + async run(context) { + try { + const file = context.propsValue.file; + const textItems = context.propsValue.textItems as Array<{ + text: string; + applyToAllPages?: boolean; + pageNumber?: number; + distanceFromLeft: number; + distanceFromTop: number; + font: StandardFonts; + fontSize: number; + lineSpacing?: number; + }>; + + const pdfDoc = await PDFDocument.load(file.data as any); + const pages = pdfDoc.getPages(); + const totalPages = pages.length; + + const embeddedFonts: Record = {}; + + for (const item of textItems) { + const cleanText = item.text.replace(/\r\n|\r/g, '\n'); + const lines = cleanText.split('\n'); + const cleanTextSample = cleanText.substring(0, 15); + const lineSpacing = item.lineSpacing ?? 1.15; + + if (lineSpacing <= 0) { + throw new Error( + `Line Spacing must be a positive number greater than 0. You provided ${lineSpacing} for text "${cleanTextSample}..."` + ); + } + + if (item.fontSize <= 0) { + throw new Error( + `Font Size must be a positive number greater than 0. You provided ${item.fontSize} for text "${cleanTextSample}..."` + ); + } + + const actualLineHeight = item.fontSize * lineSpacing; + + const fontEnum = item.font; + if (!embeddedFonts[fontEnum]) { + embeddedFonts[fontEnum] = await pdfDoc.embedFont(fontEnum); + } + const font = embeddedFonts[fontEnum]; + + const maxTextWidth = Math.max( + ...lines.map(line => font.widthOfTextAtSize(line, item.fontSize)) + ); + + const targetPages = []; + + if (item.applyToAllPages) { + targetPages.push(...pages); + } else { + if (item.pageNumber === undefined) { + throw new Error( + `Page Number is required when "Apply to all pages?" is not checked for text "${cleanTextSample}...".` + ); + } + const pageIndex = Number(item.pageNumber) - 1; + + if (pageIndex < 0 || pageIndex >= totalPages) { + throw new Error( + `You requested Page ${item.pageNumber} for text "${cleanTextSample}...", but this document only has ${totalPages} page(s).` + ); + } + targetPages.push(pages[pageIndex]); + } + + for (const targetPage of targetPages) { + const { width, height } = targetPage.getSize(); + + const pdfY = height - item.distanceFromTop; + + // Check Left Edge + if (item.distanceFromLeft < 0 || item.distanceFromLeft > width) { + throw new Error( + `The Left distance (${item.distanceFromLeft}pts) for "${cleanTextSample}..." is outside the page width. Current PDF's max width is ${Math.round(width)}pts.` + ); + } + + // Check Right Edge (Long text running off the right edge) + if (item.distanceFromLeft + maxTextWidth > width) { + throw new Error( + `The text "${cleanTextSample}..." is too long and runs off the right edge. Reduce the Left distance or font size.` + ); + } + + // Check Top Edge + if (pdfY < 0 || pdfY > height) { + throw new Error( + `The Top distance (${item.distanceFromTop}pts) for "${cleanTextSample}..." is outside the page height. Current PDF's max height is ${Math.round(height)}pts.` + ); + } + + // Check Bottom Edge (Multi-line text running off the bottom) + const lowestYPosition = pdfY - ((lines.length - 1) * actualLineHeight); + + if (lowestYPosition < 0) { + throw new Error( + `The text "${cleanTextSample}..." contains too many lines and runs off the bottom of the page. Reduce the Top distance, Font Size, or Line Spacing.` + ); + } + + targetPage.drawText(cleanText, { + x: item.distanceFromLeft, + y: pdfY, + size: item.fontSize, + font: font, + color: rgb(0, 0, 0), + lineHeight: actualLineHeight, + }); + } + } + + const pdfBytes = await pdfDoc.save(); + const base64Pdf = Buffer.from(pdfBytes).toString('base64'); + + return context.files.write({ + data: Buffer.from(base64Pdf, 'base64'), + fileName: `stamped_${file.filename}`, + }); + } catch (error) { + throw new Error(`Failed to add text to PDF: ${(error as Error).message}`); + } + }, +}); \ No newline at end of file diff --git a/packages/server/api/src/app/mcp/mcp-service.ts b/packages/server/api/src/app/mcp/mcp-service.ts index 51bca448b0c..cfe49944594 100644 --- a/packages/server/api/src/app/mcp/mcp-service.ts +++ b/packages/server/api/src/app/mcp/mcp-service.ts @@ -1,6 +1,6 @@ import { rejectedPromiseHandler } from '@activepieces/server-common' -import { apId, FlowStatus, FlowTriggerType, FlowVersionState, isNil, MCP_TRIGGER_PIECE_NAME, McpProperty, McpPropertyType, McpServer as McpServerSchema, McpServerStatus, McpTrigger, PopulatedFlow, PopulatedMcpServer, sanitizeToolName, TelemetryEventName } from '@activepieces/shared' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { apId, createToolName, FlowStatus, FlowTriggerType, FlowVersionState, isNil, MCP_TRIGGER_PIECE_NAME, McpProperty, McpPropertyType, McpServer as McpServerSchema, McpServerStatus, McpTrigger, PopulatedFlow, PopulatedMcpServer, TelemetryEventName } from '@activepieces/shared' +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' import { FastifyBaseLogger } from 'fastify' import { z } from 'zod' import { repoFactory } from '../core/db/repo-factory' @@ -61,7 +61,7 @@ export const mcpServerService = (log: FastifyBaseLogger) => { const zodFromInputSchema = Object.fromEntries(mcpInputs.map((property) => [property.name, mcpPropertyToZod(property)])) const baseName = (mcpTrigger.input?.toolName ?? flow.version.displayName) + '_' + flow.id.substring(0, 4) - const toolName = sanitizeToolName(baseName) + const toolName = createToolName(baseName) const toolDescription: string = mcpTrigger.input?.toolDescription ?? '' server.tool(toolName, toolDescription, zodFromInputSchema, { title: toolName }, async (args) => { @@ -119,6 +119,7 @@ export const mcpServerService = (log: FastifyBaseLogger) => { }) } + registerEmptyResourcesAndPrompts(server) return server }, } @@ -167,6 +168,27 @@ function mcpPropertyToZod(property: McpProperty): z.ZodTypeAny { return property.required ? schema : schema.nullish() } +/** + * Registers resources/list and prompts/list so they return empty lists. + * + * - Resources: register a resource template with an empty list. + * - Prompts: register an empty prompt so the handler is set and returns []. + * + * Claude Desktop (mcp-remote) does not support prompts/list, so we register an empty prompt. + */ +function registerEmptyResourcesAndPrompts(server: McpServer): void { + server.registerResource( + '_', + new ResourceTemplate('activepieces://empty', { + list: async () => ({ resources: [] }), + }), + {}, + async () => ({ contents: [] }), + ) + server.registerPrompt('_', {}, () => ({ messages: [] })) +} + + type BuildServerRequest = { mcp: PopulatedMcpServer diff --git a/packages/shared/package.json b/packages/shared/package.json index 10088d68c6a..22e47c19f2e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/shared", - "version": "0.40.1", + "version": "0.41.0", "type": "commonjs", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", diff --git a/packages/shared/src/lib/automation/agents/tools.ts b/packages/shared/src/lib/automation/agents/tools.ts index dc2c42b61db..ebf25472262 100644 --- a/packages/shared/src/lib/automation/agents/tools.ts +++ b/packages/shared/src/lib/automation/agents/tools.ts @@ -7,7 +7,7 @@ export const TASK_COMPLETION_TOOL_NAME = 'updateTaskStatus' * Normalizes a string for use as an agent tool name: safe characters only, * collapsed underscores, max 60 chars, lowercase, and appends '_mcp'. */ -export function sanitizeToolName(name: string): string { +export function createToolName(name: string): string { return String(name) .replace(/[^a-zA-Z0-9_-]/g, '_') .replace(/_+/g, '_') diff --git a/packages/web/src/app/builder/step-settings/agent-settings/piece-tool-dialog.tsx b/packages/web/src/app/builder/step-settings/agent-settings/piece-tool-dialog.tsx index 3be689c9a65..74b298ff893 100644 --- a/packages/web/src/app/builder/step-settings/agent-settings/piece-tool-dialog.tsx +++ b/packages/web/src/app/builder/step-settings/agent-settings/piece-tool-dialog.tsx @@ -1,4 +1,4 @@ -import { AgentTool, isNil, sanitizeToolName } from '@activepieces/shared'; +import { AgentTool, isNil, createToolName } from '@activepieces/shared'; import { t } from 'i18next'; import { ChevronLeft } from 'lucide-react'; import { useMemo, useEffect } from 'react'; @@ -86,7 +86,6 @@ export function AgentPieceDialog({ useEffect(() => { if (!showAddPieceDialog) return; - if (!isNil(editingPieceTool) && pieceMetadata.length > 0) { const piece = pieceMetadata.find( (p) => p.pieceName === editingPieceTool.pieceMetadata.pieceName, @@ -94,13 +93,12 @@ export function AgentPieceDialog({ if (piece) { handlePieceSelect(piece); - - const action = piece.suggestedActions?.find( - (a) => - sanitizeToolName(`${piece.pieceName}-${a.name}`) === - sanitizeToolName(editingPieceTool.toolName), - ); - + const action = piece.suggestedActions?.find((a) => { + return ( + createToolName(`${piece.pieceName}-${a.name}`) === + editingPieceTool.toolName + ); + }); if (action) { handleActionSelect(action); } @@ -112,7 +110,6 @@ export function AgentPieceDialog({ const handleSave = () => { const newTool = createNewPieceTool(); - if (isNil(newTool)) return; if (!isNil(editingPieceTool)) { diff --git a/packages/web/src/app/components/project-settings/mcp-server/mcp-credentials.tsx b/packages/web/src/app/components/project-settings/mcp-server/mcp-credentials.tsx index b7c73b82be8..2f7111eae20 100644 --- a/packages/web/src/app/components/project-settings/mcp-server/mcp-credentials.tsx +++ b/packages/web/src/app/components/project-settings/mcp-server/mcp-credentials.tsx @@ -23,7 +23,8 @@ export function McpCredentials({ mcpServer }: McpCredentialsProps) { mcpHooks.useRotateMcpToken(currentProjectId!); const { data: publicUrl } = flagsHooks.useFlag(ApFlagId.PUBLIC_URL); - const serverUrl = `${publicUrl}api/v1/projects/${currentProjectId}/mcp-server/http`; + const baseUrl = publicUrl?.replace(/\/$/, '') ?? ''; + const serverUrl = `${baseUrl}/api/v1/projects/${currentProjectId}/mcp-server/http`; const maskToken = (tokenValue: string) => { if (tokenValue.length <= 8) return '••••••••'; @@ -41,6 +42,21 @@ export function McpCredentials({ mcpServer }: McpCredentialsProps) { }, }; + const claudeDesktopConfiguration = { + mcpServers: { + activepieces: { + command: 'npx', + args: [ + '-y', + 'mcp-remote', + serverUrl, + '--header', + `Authorization: Bearer ${mcpServer?.token ?? 'YOUR_TOKEN'}`, + ], + }, + }, + }; + return (
{/* Base URL Field */} @@ -106,12 +122,22 @@ export function McpCredentials({ mcpServer }: McpCredentialsProps) {

- {/* JSON Configuration */} + {/* JSON Configuration (Cursor / URL + headers) */} + + {/* Claude Desktop (mcp-remote) */} + diff --git a/packages/web/src/features/agents/agent-tools/componenets/piece-tool.tsx b/packages/web/src/features/agents/agent-tools/componenets/piece-tool.tsx index 5f7f8e4baf9..a6a37b6b4c4 100644 --- a/packages/web/src/features/agents/agent-tools/componenets/piece-tool.tsx +++ b/packages/web/src/features/agents/agent-tools/componenets/piece-tool.tsx @@ -1,4 +1,4 @@ -import { AgentPieceTool, sanitizeToolName } from '@activepieces/shared'; +import { AgentPieceTool, createToolName } from '@activepieces/shared'; import { t } from 'i18next'; import { Plus, Puzzle, X } from 'lucide-react'; import { useMemo } from 'react'; @@ -97,9 +97,8 @@ export const AgentPieceToolComponent = ({ {tools.map((tool) => { const toolName = pieceMetadata.suggestedActions?.find( (action) => - sanitizeToolName( - `${pieceMetadata.pieceName}-${action.name}`, - ) === sanitizeToolName(tool.toolName), + createToolName(`${pieceMetadata.pieceName}-${action.name}`) === + createToolName(tool.toolName), )?.displayName; return (
= ({
{filteredActions.map((action) => { const isDisabled = selectedActionNames.has( - sanitizeToolName(`${selectedPiece.pieceName}-${action.name}`), + createToolName(`${selectedPiece.pieceName}-${action.name}`), ); return ( diff --git a/packages/web/src/features/agents/agent-tools/stores/pieces-tools.ts b/packages/web/src/features/agents/agent-tools/stores/pieces-tools.ts index 1b84f8c45e6..ec91677cc25 100644 --- a/packages/web/src/features/agents/agent-tools/stores/pieces-tools.ts +++ b/packages/web/src/features/agents/agent-tools/stores/pieces-tools.ts @@ -4,7 +4,7 @@ import { AgentToolType, isNil, PredefinedInputsStructure, - sanitizeToolName, + createToolName, } from '@activepieces/shared'; import { create } from 'zustand'; @@ -129,7 +129,7 @@ export const usePieceToolsDialogStore = create( return { type: AgentToolType.PIECE, - toolName: sanitizeToolName( + toolName: createToolName( `${selectedPiece.pieceName}-${selectedAction.name}`, ), pieceMetadata: {