Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,146 @@
import {
AgentResult,
AgentStepBlock,
AgentTaskStatus,
AgentTool,
AgentToolType,
assertNotNullOrUndefined,
ContentBlockType,
isNil,
MarkdownContentBlock,
ToolCallBase,
ToolCallContentBlock,
ToolCallStatus,
ToolCallType,
} from '@activepieces/shared';
export const agentOutputBuilder = (prompt: string) => {
let status: AgentTaskStatus = AgentTaskStatus.IN_PROGRESS;
const steps: AgentStepBlock[] = [];
let structuredOutput: Record<string, unknown> | undefined = undefined;
return {
fail({ message }: FinishParams) {
if (!isNil(message)) {
this.addMarkdown(message);
}
status = AgentTaskStatus.FAILED;
},
setStatus(_status: AgentTaskStatus) {
status = _status;
},
setStructuredOutput(output: Record<string, unknown>) {
structuredOutput = output;
},
addMarkdown(markdown: string) {
if (
steps.length === 0 ||
steps[steps.length - 1].type !== ContentBlockType.MARKDOWN
) {
steps.push({
type: ContentBlockType.MARKDOWN,
markdown: '',
});
}
(steps[steps.length - 1] as MarkdownContentBlock).markdown += markdown;
},
startToolCall({
toolName,
toolCallId,
input,
agentTools,
}: StartToolCallParams) {
const metadata = getToolMetadata({
toolName,
baseTool: {
toolName,
toolCallId,
type: ContentBlockType.TOOL_CALL,
status: ToolCallStatus.IN_PROGRESS,
input,
output: undefined,
startTime: new Date().toISOString(),
},
tools: agentTools,
});
steps.push(metadata);
},
finishToolCall({ toolCallId, output }: FinishToolCallParams) {
const toolIdx = steps.findIndex(
(block) =>
block.type === ContentBlockType.TOOL_CALL &&
(block as ToolCallContentBlock).toolCallId === toolCallId
);
const tool = steps[toolIdx] as ToolCallContentBlock;
assertNotNullOrUndefined(tool, 'Last block must be a tool call');
steps[toolIdx] = {
...tool,
status: ToolCallStatus.COMPLETED,
endTime: new Date().toISOString(),
output,
};
},
build(): AgentResult {
return {
status,
steps,
structuredOutput,
prompt,
};
},
};
};
type FinishToolCallParams = {
toolCallId: string;
output: Record<string, unknown>;
};
type StartToolCallParams = {
toolName: string;
toolCallId: string;
input: Record<string, unknown>;
agentTools: AgentTool[];
};
type FinishParams = {
message?: string;
};
function getToolMetadata({
toolName,
tools,
baseTool,
}: GetToolMetadaParams): ToolCallContentBlock {
const tool = tools.find((tool) => tool.toolName === toolName);
assertNotNullOrUndefined(tool, `Tool ${toolName} not found`);
switch (tool.type) {
case AgentToolType.PIECE: {
const pieceMetadata = tool.pieceMetadata;
assertNotNullOrUndefined(pieceMetadata, 'Piece metadata is required');
return {
...baseTool,
toolCallType: ToolCallType.PIECE,
pieceName: pieceMetadata.pieceName,
pieceVersion: pieceMetadata.pieceVersion,
actionName: tool.pieceMetadata.actionName,
};
}
case AgentToolType.FLOW: {
assertNotNullOrUndefined(tool.flowId, 'Flow ID is required');
return {
...baseTool,
toolCallType: ToolCallType.FLOW,
displayName: 'Unknown',
flowId: tool.flowId,
};
}
}
}
type GetToolMetadaParams = {
toolName: string;
tools: AgentTool[];
baseTool: ToolCallBase;
}

View File

@@ -0,0 +1,245 @@
import {
createAction,
Property,
PieceAuth,
} from '@activepieces/pieces-framework';
import {
AgentOutputField,
AgentOutputFieldType,
AgentPieceProps,
AgentTaskStatus,
isNil,
AgentTool,
TASK_COMPLETION_TOOL_NAME,
} from '@activepieces/shared';
import { dynamicTool, hasToolCall, stepCountIs, streamText } from 'ai';
import { z, ZodObject } from 'zod';
import { agentOutputBuilder } from './agent-output-builder';
import { createAIModel } from '../../common/ai-sdk';
import { aiProps } from '../../common/props';
import { inspect } from 'util';
export const runAgent = createAction({
name: 'run_agent',
displayName: 'Run Agent',
description: 'Run the AI assistant to complete your task.',
auth: PieceAuth.None(),
props: {
[AgentPieceProps.PROMPT]: Property.LongText({
displayName: 'Prompt',
description: 'Describe what you want the assistant to do.',
required: true,
}),
[AgentPieceProps.AI_PROVIDER]: aiProps({ modelType: 'text' }).provider,
[AgentPieceProps.AI_MODEL]: aiProps({ modelType: 'text' }).model,
[AgentPieceProps.AGENT_TOOLS]: Property.Array({
displayName: 'Agent Tools',
required: false,
properties: {
type: Property.ShortText({
displayName: 'Tool Type',
required: true,
}),
toolName: Property.ShortText({
displayName: 'Tool Name',
required: true,
}),
pieceMetadata: Property.Json({
displayName: 'Piece Metadata',
required: false,
}),
flowId: Property.ShortText({
displayName: 'Flow Id',
required: false,
}),
},
}),
[AgentPieceProps.STRUCTURED_OUTPUT]: Property.Array({
displayName: 'Structured Output',
defaultValue: undefined,
required: false,
properties: {
displayName: Property.ShortText({
displayName: 'Display Name',
required: true,
}),
description: Property.ShortText({
displayName: 'Description',
required: false,
}),
type: Property.ShortText({
displayName: 'Type',
required: true,
}),
},
}),
[AgentPieceProps.MAX_STEPS]: Property.Number({
displayName: 'Max steps',
description: 'The numbder of interations the agent can do',
required: true,
defaultValue: 20,
}),
},
async run(context) {
const { prompt, maxSteps, model: modelId, provider: providerId } = context.propsValue;
const model = await createAIModel({
modelId,
providerId,
engineToken: context.server.token,
apiUrl: context.server.apiUrl,
});
const outputBuilder = agentOutputBuilder(prompt);
const hasStructuredOutput =
!isNil(context.propsValue.structuredOutput) &&
context.propsValue.structuredOutput.length > 0;
const agentToolsMetadata = context.propsValue.agentTools as AgentTool[];
const agentTools = await context.agent.tools({
tools: agentToolsMetadata,
model: model,
});
const stream = streamText({
model: model,
prompt: `
${prompt}
<important_note>
As your FINAL ACTION, you must call the \`${TASK_COMPLETION_TOOL_NAME}\` tool to indicate if the task is complete or not.
Call this tool only once you have done everything you can to achieve the user's goal, or if you are unable to continue.
If you do not make this final call, your work will be considered unsuccessful.
</important_note>
`,
system: `
You are a helpful, proactive AI assistant.
Today's date is ${new Date().toISOString().split('T')[0]}.
Help the user finish their goal quickly and accurately.
`.trim(),
stopWhen: [stepCountIs(maxSteps), hasToolCall(TASK_COMPLETION_TOOL_NAME)],
tools: {
...agentTools,
[TASK_COMPLETION_TOOL_NAME]: dynamicTool({
description:
"This tool must be called as your FINAL ACTION to indicate whether the assigned goal was accomplished. Call it only when you have completed the user's task, or if you are unable to continue. Once you call this tool, you should not take any further actions.",
inputSchema: z.object({
success: z
.boolean()
.describe(
'Set to true if the assigned goal was achieved, or false if the task was abandoned or failed.'
),
...(hasStructuredOutput
? {
output: z
.object(
structuredOutputSchema(
context.propsValue
.structuredOutput as AgentOutputField[]
)?.shape ?? {}
)
.nullable()
.describe(
'The structured output of your task. This is optional and can be omitted if you have not achieved the goal.'
),
}
: {
output: z
.string()
.nullable()
.describe(
'The message to the user with the result of your task. This is optional and can be omitted if you have not achieved the goal.'
),
}),
}),
execute: async (params) => {
const { success, output } = params as {
success: boolean;
output?: Record<string, unknown>;
};
outputBuilder.setStatus(
success ? AgentTaskStatus.COMPLETED : AgentTaskStatus.FAILED
);
if (hasStructuredOutput && !isNil(output)) {
outputBuilder.setStructuredOutput(output);
}
if (!hasStructuredOutput && !isNil(output)) {
outputBuilder.addMarkdown(output as unknown as string);
}
return {};
},
}),
},
});
for await (const chuck of stream.fullStream) {
switch (chuck.type) {
case 'text-delta': {
outputBuilder.addMarkdown(chuck.text);
break;
}
case 'tool-call': {
if (isTaskCompletionToolCall(chuck.toolName)) {
continue;
}
outputBuilder.startToolCall({
toolName: chuck.toolName,
toolCallId: chuck.toolCallId,
input: chuck.input as Record<string, unknown>,
agentTools: agentToolsMetadata,
});
break;
}
case 'tool-result': {
if (isTaskCompletionToolCall(chuck.toolName)) {
continue;
}
outputBuilder.finishToolCall({
toolCallId: chuck.toolCallId,
output: chuck.output as Record<string, unknown>,
});
break;
}
case 'error': {
outputBuilder.fail({
message: 'Error running agent: ' + inspect(chuck.error),
});
break;
}
}
await context.output.update({ data: outputBuilder.build() });
}
const { status } = outputBuilder.build();
if (status == AgentTaskStatus.IN_PROGRESS) {
outputBuilder.fail({});
}
return outputBuilder.build();
},
});
const isTaskCompletionToolCall = (toolName: string) =>
toolName === TASK_COMPLETION_TOOL_NAME;
function structuredOutputSchema(
outputFields: AgentOutputField[]
): ZodObject | undefined {
const shape: Record<string, z.ZodType> = {};
for (const field of outputFields) {
switch (field.type) {
case AgentOutputFieldType.TEXT:
shape[field.displayName] = z.string();
break;
case AgentOutputFieldType.NUMBER:
shape[field.displayName] = z.number();
break;
case AgentOutputFieldType.BOOLEAN:
shape[field.displayName] = z.boolean();
break;
default:
shape[field.displayName] = z.any();
}
}
return Object.keys(shape).length > 0 ? z.object(shape) : undefined;
}

View File

@@ -0,0 +1,278 @@
import {
ApFile,
createAction,
DynamicPropsValue,
InputPropertyMap,
PieceAuth,
Property,
} from '@activepieces/pieces-framework';
import {
GeneratedFile,
generateText,
GenerateTextResult,
ImagePart,
ToolSet,
} from 'ai';
import { experimental_generateImage as generateImage } from 'ai';
import { LanguageModelV2 } from '@ai-sdk/provider';
import mime from 'mime-types';
import { isNil } from '@activepieces/shared';
import { createAIModel } from '../../common/ai-sdk';
import { AIProviderName } from '../../common/types';
import { aiProps } from '../../common/props';
export const generateImageAction = createAction({
name: 'generateImage',
displayName: 'Generate Image',
description: '',
props: {
provider: aiProps({ modelType: 'image' }).provider,
model: aiProps({ modelType: 'image' }).model,
prompt: Property.LongText({
displayName: 'Prompt',
required: true,
}),
advancedOptions: Property.DynamicProperties({
displayName: 'Advanced Options',
required: false,
auth: PieceAuth.None(),
refreshers: ['provider', 'model'],
props: async (propsValue): Promise<InputPropertyMap> => {
const providerId = propsValue['provider'] as unknown as string;
const modelId = propsValue['model'] as unknown as string;
let options: InputPropertyMap = {};
if (providerId === AIProviderName.OPENAI) {
options = {
quality: Property.StaticDropdown({
options: {
options:
modelId === 'dall-e-3'
? [
{ label: 'Standard', value: 'standard' },
{ label: 'HD', value: 'hd' },
]
: modelId === 'gpt-image-1'
? [
{ label: 'High', value: 'high' },
{ label: 'Medium', value: 'medium' },
{ label: 'Low', value: 'low' },
]
: [],
disabled: modelId === 'dall-e-2',
},
defaultValue: modelId === 'dall-e-3' ? 'standard' : 'high',
displayName: 'Image Quality',
required: false,
}),
size: Property.StaticDropdown({
options: {
options:
modelId === 'dall-e-3'
? [
{ label: '1024x1024', value: '1024x1024' },
{ label: '1792x1024', value: '1792x1024' },
{ label: '1024x1792', value: '1024x1792' },
]
: modelId === 'gpt-image-1'
? [
{ label: '1024x1024', value: '1024x1024' },
{ label: '1536x1024', value: '1536x1024' },
{ label: '1024x1536', value: '1024x1536' },
]
: [
{ label: '256x256', value: '256x256' },
{ label: '512x512', value: '512x512' },
{ label: '1024x1024', value: '1024x1024' },
],
},
displayName: 'Image Size',
required: false,
}),
};
if (modelId === 'gpt-image-1') {
options = {
...options,
background: Property.StaticDropdown({
options: {
options: [
{ label: 'Auto', value: 'auto' },
{ label: 'Transparent', value: 'transparent' },
{ label: 'Opaque', value: 'opaque' },
],
},
defaultValue: 'auto',
description: 'The background of the image.',
displayName: 'Background',
required: true,
}),
};
}
return options;
}
if (
providerId === AIProviderName.GOOGLE &&
modelId === 'gemini-2.5-flash-image-preview'
) {
options = {
image: Property.Array({
displayName: 'Images',
required: false,
properties: {
file: Property.File({
displayName: 'Image File',
required: true,
}),
},
description: 'The image(s) you want to edit/merge',
}),
};
}
return options;
},
}),
},
async run(context) {
const providerId = context.propsValue.provider;
const modelId = context.propsValue.model;
const image = await getGeneratedImage({
providerId,
modelId,
engineToken: context.server.token,
apiUrl: context.server.apiUrl,
prompt: context.propsValue.prompt,
advancedOptions: context.propsValue.advancedOptions,
});
const imageData =
image.base64 && image.base64.length > 0
? Buffer.from(image.base64, 'base64')
: Buffer.from(image.uint8Array);
return context.files.write({
data: imageData,
fileName: 'image.png',
});
},
});
const getGeneratedImage = async ({
providerId,
modelId,
engineToken,
apiUrl,
prompt,
advancedOptions,
}: {
providerId: string;
modelId: string;
engineToken: string;
apiUrl: string;
prompt: string;
advancedOptions?: DynamicPropsValue;
}): Promise<GeneratedFile> => {
const model = await createAIModel({
providerId,
modelId,
engineToken,
apiUrl,
isImage: true,
});
switch (providerId) {
case AIProviderName.GOOGLE:
case AIProviderName.ACTIVEPIECES:
case AIProviderName.OPENROUTER:
return generateImageUsingGenerateText({
model: model as unknown as LanguageModelV2,
prompt,
advancedOptions,
});
default: {
const { image } = await generateImage({
model,
prompt,
providerOptions: {
[providerId]: { ...advancedOptions },
},
});
return image
};
}
};
const generateImageUsingGenerateText = async ({
model,
prompt,
advancedOptions,
}: {
model: LanguageModelV2;
prompt: string;
advancedOptions?: DynamicPropsValue;
}): Promise<GeneratedFile> => {
const images =
(advancedOptions?.['image'] as Array<{ file: ApFile }> | undefined) ?? [];
const imageFiles = images.map<ImagePart>((image) => {
const fileType = image.file.extension
? mime.lookup(image.file.extension)
: 'image/jpeg';
return {
type: 'image',
image: `data:${fileType};base64,${image.file.base64}`,
};
});
const result = await generateText({
model,
providerOptions: {
google: { responseModalities: ['TEXT', 'IMAGE'] },
openrouter: { modalities: ['image', 'text'] },
},
messages: [
{
role: 'user',
content: [{ type: 'text', text: prompt }, ...imageFiles],
},
],
});
assertImageGenerationSuccess(result);
return result.files[0];
};
const assertImageGenerationSuccess = (
result: GenerateTextResult<ToolSet, never>
): void => {
const responseBody =
result.response.body &&
typeof result.response.body === 'object' &&
'candidates' in result.response.body
? result.response.body
: { candidates: [] };
const responseCandidates = Array.isArray(responseBody?.candidates)
? responseBody?.candidates
: [];
responseCandidates.forEach((candidate: { finishReason: string }) => {
if (candidate.finishReason !== 'STOP') {
throw new Error(
'Image generation failed Reason:\n ' +
JSON.stringify(responseCandidates, null, 2)
);
}
});
if (isNil(result.files) || result.files.length === 0) {
throw new Error('No image generated');
}
};

View File

@@ -0,0 +1,337 @@
import {
createAction,
InputPropertyMap,
PieceAuth,
Property,
} from '@activepieces/pieces-framework';
import { ModelMessage, ToolSet, generateText, stepCountIs } from 'ai';
import { spreadIfDefined } from '@activepieces/shared';
import { aiProps } from '../../common/props';
import { AIProviderName } from '../../common/types';
import { anthropicSearchTool, openaiSearchTool, googleSearchTool, createAIModel } from '../../common/ai-sdk';
export const askAI = createAction({
name: 'askAi',
displayName: 'Ask AI',
description: '',
props: {
provider: aiProps({ modelType: 'text' }).provider,
model: aiProps({ modelType: 'text' }).model,
prompt: Property.LongText({
displayName: 'Prompt',
required: true,
}),
conversationKey: Property.ShortText({
displayName: 'Conversation Key',
required: false,
}),
creativity: Property.Number({
displayName: 'Creativity',
required: false,
defaultValue: 100,
description:
'Controls the creativity of the AI response. A higher value will make the AI more creative and a lower value will make it more deterministic.',
}),
maxOutputTokens: Property.Number({
displayName: 'Max Tokens',
required: false,
defaultValue: 2000,
}),
webSearch: Property.Checkbox({
displayName: 'Web Search',
required: false,
defaultValue: false,
description:
'Whether to use web search to find information for the AI to use in its response.',
}),
webSearchOptions: Property.DynamicProperties({
displayName: 'Web Search Options',
required: false,
auth: PieceAuth.None(),
refreshers: ['webSearch', 'provider', 'model'],
props: async (propsValue) => {
const webSearchEnabled = propsValue['webSearch'] as unknown as boolean;
const provider = propsValue['provider'] as unknown as string;
if (!webSearchEnabled) {
return {};
}
let options: InputPropertyMap = {
maxUses: Property.Number({
displayName: 'Max Web Search Uses',
required: false,
defaultValue: 5,
description: 'Maximum number of searches to use. Default is 5.',
}),
includeSources: Property.Checkbox({
displayName: 'Include Sources',
description:
'Whether to include the sources in the response. Useful for getting web search details (e.g. search queries, searched URLs, etc).',
required: false,
defaultValue: false,
}),
};
const userLocationOptions = {
userLocationCity: Property.ShortText({
displayName: 'User Location - City',
required: false,
description:
'The city name for localizing search results (e.g., San Francisco).',
}),
userLocationRegion: Property.ShortText({
displayName: 'User Location - Region',
required: false,
description:
'The region or state for localizing search results (e.g., California).',
}),
userLocationCountry: Property.ShortText({
displayName: 'User Location - Country',
required: false,
description:
'The country code for localizing search results (e.g., US).',
}),
userLocationTimezone: Property.ShortText({
displayName: 'User Location - Timezone',
required: false,
description:
'The IANA timezone ID for localizing search results (e.g., America/Los_Angeles).',
}),
};
if (provider === AIProviderName.ANTHROPIC) {
options = {
...options,
allowedDomains: Property.Array({
displayName: 'Allowed Domains',
required: false,
description:
'List of domains to search (e.g., example.com, docs.example.com/blog). Domains should not include HTTP/HTTPS scheme. Subdomains are automatically included unless more specific subpaths are provided. Overrides Blocked Domains if both are provided.',
properties: {
domain: Property.ShortText({
displayName: 'Domain',
required: true,
}),
},
}),
blockedDomains: Property.Array({
displayName: 'Blocked Domains',
required: false,
description:
'List of domains to exclude from search (e.g., example.com, docs.example.com/blog). Domains should not include HTTP/HTTPS scheme. Subdomains are automatically included unless more specific subpaths are provided. Overrided by Allowed Domains if both are provided.',
properties: {
domain: Property.ShortText({
displayName: 'Domain',
required: true,
}),
},
}),
...userLocationOptions,
};
}
if (provider === AIProviderName.OPENAI) {
options = {
...options,
searchContextSize: Property.StaticDropdown({
displayName: 'Search Context Size',
required: false,
defaultValue: 'medium',
options: {
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
},
description:
'High level guidance for the amount of context window space to use for the search.',
}),
...userLocationOptions,
};
}
return options;
},
}),
},
async run(context) {
const providerId = context.propsValue.provider;
const modelId = context.propsValue.model;
const storage = context.store;
const webSearchOptions = context.propsValue.webSearchOptions as WebSearchOptions;
const model = await createAIModel({
providerId,
modelId,
engineToken: context.server.token,
apiUrl: context.server.apiUrl,
openaiResponsesModel: true,
});
const conversationKey = context.propsValue.conversationKey
? `ask-ai-conversation:${context.propsValue.conversationKey}`
: null;
let conversation = null;
if (conversationKey) {
conversation = (await storage.get<ModelMessage[]>(conversationKey)) ?? [];
if (!conversation) {
await storage.put(conversationKey, { messages: [] });
}
}
const response = await generateText({
model,
messages: [
...(conversation ?? []),
{
role: 'user',
content: context.propsValue.prompt,
},
],
maxOutputTokens: context.propsValue.maxOutputTokens,
temperature: (context.propsValue.creativity ?? 100) / 100,
tools: context.propsValue.webSearch
? createWebSearchTool(providerId, webSearchOptions)
: undefined,
stopWhen: stepCountIs(webSearchOptions?.maxUses ?? 5),
});
conversation?.push({
role: 'user',
content: context.propsValue.prompt,
});
conversation?.push({
role: 'assistant',
content: response.text ?? '',
});
if (conversationKey) {
await storage.put(conversationKey, conversation);
}
const includeSources = webSearchOptions.includeSources;
if (includeSources) {
return { text: response.text, sources: response.sources };
}
return response.text;
},
});
export function createWebSearchTool(
provider: string,
options: WebSearchOptions = {}
): ToolSet {
const defaultMaxUses = 5;
switch (provider) {
case AIProviderName.ANTHROPIC: {
const anthropicOptions = options as AnthropicWebSearchOptions;
let allowedDomains: string[] | undefined;
let blockedDomains: string[] | undefined;
if (
anthropicOptions.allowedDomains &&
anthropicOptions.allowedDomains.length > 0
) {
allowedDomains = anthropicOptions.allowedDomains.map(
({ domain }) => domain
);
}
if (
anthropicOptions.blockedDomains &&
anthropicOptions.blockedDomains.length > 0 &&
(!anthropicOptions.allowedDomains ||
anthropicOptions.allowedDomains.length === 0)
) {
blockedDomains = anthropicOptions.blockedDomains.map(
({ domain }) => domain
);
}
return {
web_search: anthropicSearchTool({
maxUses: anthropicOptions.maxUses ?? defaultMaxUses,
...spreadIfDefined(
'userLocation',
buildUserLocation(anthropicOptions)
),
...spreadIfDefined('allowedDomains', allowedDomains),
...spreadIfDefined('blockedDomains', blockedDomains),
}),
} as any;
}
case AIProviderName.OPENAI: {
const openaiOptions = options as OpenAIWebSearchOptions;
return {
web_search_preview: openaiSearchTool({
...spreadIfDefined(
'searchContextSize',
openaiOptions.searchContextSize
),
...spreadIfDefined('userLocation', buildUserLocation(openaiOptions)),
}),
} as any;
}
case AIProviderName.GOOGLE: {
return {
google_search: googleSearchTool({}),
} as any;
}
default:
throw new Error(`Provider ${provider} is not supported for web search`);
}
}
function buildUserLocation(
options: UserLocationOptions
): (UserLocationOptions & { type: 'approximate' }) | undefined {
if (
!options.userLocationCity &&
!options.userLocationRegion &&
!options.userLocationCountry &&
!options.userLocationTimezone
) {
return undefined;
}
return {
type: 'approximate' as const,
...spreadIfDefined('city', options.userLocationCity),
...spreadIfDefined('region', options.userLocationRegion),
...spreadIfDefined('country', options.userLocationCountry),
...spreadIfDefined('timezone', options.userLocationTimezone),
};
}
type BaseWebSearchOptions = {
maxUses?: number
includeSources?: boolean
}
type UserLocationOptions = {
userLocationCity?: string
userLocationRegion?: string
userLocationCountry?: string
userLocationTimezone?: string
}
type AnthropicWebSearchOptions = BaseWebSearchOptions & UserLocationOptions & {
allowedDomains?: { domain: string }[]
blockedDomains?: { domain: string }[]
}
type OpenAIWebSearchOptions = BaseWebSearchOptions & UserLocationOptions & {
searchContextSize?: 'low' | 'medium' | 'high'
}
export type WebSearchOptions = AnthropicWebSearchOptions | OpenAIWebSearchOptions

View File

@@ -0,0 +1,60 @@
import { AIProviderName } from '../../common/types';
import { createAIModel } from '../../common/ai-sdk';
import { createAction, Property } from '@activepieces/pieces-framework';
import { generateText } from 'ai';
import { aiProps } from '../../common/props';
export const summarizeText = createAction({
name: 'summarizeText',
displayName: 'Summarize Text',
description: '',
props: {
provider: aiProps({ modelType: 'text' }).provider,
model: aiProps({ modelType: 'text' }).model,
text: Property.LongText({
displayName: 'Text',
required: true,
}),
prompt: Property.ShortText({
displayName: 'Prompt',
defaultValue:
'Summarize the following text in a clear and concise manner, capturing the key points and main ideas while keeping the summary brief and informative.',
required: true,
}),
maxOutputTokens: Property.Number({
displayName: 'Max Tokens',
required: false,
defaultValue: 2000,
}),
},
async run(context) {
const providerId = context.propsValue.provider;
const modelId = context.propsValue.model;
const model = await createAIModel({
providerId,
modelId,
engineToken: context.server.token,
apiUrl: context.server.apiUrl,
});
const response = await generateText({
model,
messages: [
{
role: 'user',
content: `${context.propsValue.prompt} Summarize the following text : ${context.propsValue.text}`
},
],
maxOutputTokens: context.propsValue.maxOutputTokens,
temperature: 1,
providerOptions: {
[providerId]: {
...(providerId === AIProviderName.OPENAI ? { reasoning_effort: 'minimal' } : {}),
}
}
});
return response.text ?? '';
},
});

View File

@@ -0,0 +1,53 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { generateText } from 'ai';
import { createAIModel } from '../../common/ai-sdk';
import { aiProps } from '../../common/props';
export const classifyText = createAction({
name: 'classifyText',
displayName: 'Classify Text',
description: 'Classify your text into one of your provided categories.',
props: {
provider: aiProps({ modelType: 'text' }).provider,
model: aiProps({ modelType: 'text' }).model,
text: Property.LongText({
displayName: 'Text to Classify',
required: true,
}),
categories: Property.Array({
displayName: 'Categories',
description: 'Categories to classify text into.',
required: true,
}),
},
async run(context) {
const categories = (context.propsValue.categories as string[]) ?? [];
const providerId = context.propsValue.provider;
const modelId = context.propsValue.model;
const model = await createAIModel({
providerId,
modelId,
engineToken: context.server.token,
apiUrl: context.server.apiUrl,
});
const response = await generateText({
model,
prompt: `As a text classifier, your task is to assign one of the following categories to the provided text: ${categories.join(
', '
)}. Please respond with only the selected category as a single word, and nothing else.
Text to classify: "${context.propsValue.text}"`,
});
const result = response.text.trim();
if (!categories.includes(result)) {
throw new Error(
'Unable to classify the text into the provided categories.'
);
}
return result;
},
});

View File

@@ -0,0 +1,284 @@
import { ApFile, createAction, PieceAuth, Property } from '@activepieces/pieces-framework';
import { createAIModel } from '../../common/ai-sdk';
import { generateText, tool, jsonSchema, ModelMessage, UserModelMessage } from 'ai';
import mime from 'mime-types';
import Ajv from 'ajv';
import { aiProps } from '../../common/props';
export const extractStructuredData = createAction({
name: 'extractStructuredData',
displayName: 'Extract Structured Data',
description: 'Extract structured data from provided text,image or PDF.',
props: {
provider: aiProps({ modelType: 'text' }).provider,
model: aiProps({ modelType: 'text' }).model,
text: Property.LongText({
displayName: 'Text',
description: 'Text to extract structured data from.',
required: false,
}),
files: Property.Array({
displayName: 'Files',
required: false,
properties: {
file: Property.File({
displayName: 'Image/PDF',
description: 'Image or PDF to extract structured data from.',
required: false,
}),
},
}),
prompt: Property.LongText({
displayName: 'Guide Prompt',
description: 'Prompt to guide the AI.',
defaultValue: 'Extract the following data from the provided data.',
required: false,
}),
mode: Property.StaticDropdown<'simple' | 'advanced'>({
displayName: 'Data Schema Type',
description: 'For complex schema, you can use advanced mode.',
required: true,
defaultValue: 'simple',
options: {
disabled: false,
options: [
{ label: 'Simple', value: 'simple' },
{ label: 'Advanced', value: 'advanced' },
],
},
}),
schema: Property.DynamicProperties({
auth: PieceAuth.None(),
displayName: 'Data Definition',
required: true,
refreshers: ['mode'],
props: async (propsValue) => {
const mode = propsValue['mode'] as unknown as 'simple' | 'advanced';
if (mode === 'advanced') {
return {
fields: Property.Json({
displayName: 'JSON Schema',
description:
'Learn more about JSON Schema here: https://json-schema.org/learn/getting-started-step-by-step',
required: true,
defaultValue: {
type: 'object',
properties: {
name: {
type: 'string',
},
age: {
type: 'number',
},
},
required: ['name'],
},
}),
};
}
return {
fields: Property.Array({
displayName: 'Data Definition',
required: true,
properties: {
name: Property.ShortText({
displayName: 'Name',
description:
'Provide the name of the value you want to extract from the unstructured text. The name should be unique and short. ',
required: true,
}),
description: Property.LongText({
displayName: 'Description',
description:
'Brief description of the data, this hints for the AI on what to look for',
required: false,
}),
type: Property.StaticDropdown({
displayName: 'Data Type',
description: 'Type of parameter.',
required: true,
defaultValue: 'string',
options: {
disabled: false,
options: [
{ label: 'Text', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Boolean', value: 'boolean' },
],
},
}),
isRequired: Property.Checkbox({
displayName: 'Fail if Not present?',
required: true,
defaultValue: false,
}),
},
}),
};
},
}),
maxOutputTokens: Property.Number({
displayName: 'Max Tokens',
required: false,
defaultValue: 2000,
}),
},
async run(context) {
const providerId = context.propsValue.provider;
const modelId = context.propsValue.model;
const text = context.propsValue.text;
const files = (context.propsValue.files as Array<{ file: ApFile }>) ?? [];
const prompt = context.propsValue.prompt;
const schema = context.propsValue.schema;
const maxOutputTokens = context.propsValue.maxOutputTokens;
if (!text && !files.length) {
throw new Error('Please provide text or image/PDF to extract data from.');
}
const model = await createAIModel({
providerId,
modelId,
engineToken: context.server.token,
apiUrl: context.server.apiUrl,
});
let schemaDefinition: any;
// Track sanitized-to-original name mapping to restore output keys.
const sanitizedNameMap: Record<string, string> = {};
if (context.propsValue.mode === 'advanced') {
const ajv = new Ajv();
const isValidSchema = ajv.validateSchema(schema['fields']);
if (!isValidSchema) {
throw new Error(
JSON.stringify({
message: 'Invalid JSON schema',
errors: ajv.errors,
}),
);
}
schemaDefinition = jsonSchema(schema['fields'] as any);
} else {
const fields = schema['fields'] as Array<{
name: string;
description?: string;
type: string;
isRequired: boolean;
}>;
const properties: Record<string, any> = {};
const required: string[] = [];
fields.forEach((field) => {
const sanitizedFieldName = field.name.replace(/[^a-zA-Z0-9_.-]/g, '_');
sanitizedNameMap[sanitizedFieldName] = field.name;
properties[sanitizedFieldName] = {
type: field.type,
description: field.description,
};
if (field.isRequired) {
required.push(sanitizedFieldName);
}
});
const jsonSchemaObject = {
type: 'object' as const,
properties,
required,
};
schemaDefinition = jsonSchema(jsonSchemaObject);
}
const extractionTool = tool({
description: 'Extract structured data from the provided content',
inputSchema: schemaDefinition,
execute: async (data) => {
return data;
},
});
const messages: Array<ModelMessage> = [];
const contentParts: UserModelMessage['content'] = [];
let textContent = prompt || 'Extract the following data from the provided data.';
if (text) {
textContent += `\n\nText to analyze:\n${text}`;
}
contentParts.push({
type: 'text',
text: textContent,
});
if (files.length > 0) {
for (const fileWrapper of files) {
const file = fileWrapper.file;
if (!file) {
continue;
}
const fileType = file.extension ? mime.lookup(file.extension) : 'image/jpeg';
if (fileType && fileType.startsWith('image') && file.base64) {
contentParts.push({
type: 'image',
image: `data:${fileType};base64,${file.base64}`,
});
} else if (fileType && fileType.startsWith('application/pdf') && file.base64) {
contentParts.push({
type: 'file',
data: `data:${fileType};base64,${file.base64}`,
mediaType: fileType,
filename: file.filename,
});
}
}
}
messages.push({
role: 'user',
content: contentParts,
});
try {
const result = await generateText({
model,
maxOutputTokens,
tools: {
extractData: extractionTool,
},
toolChoice: 'required',
messages,
});
const toolCalls = result.toolCalls;
if (!toolCalls || toolCalls.length === 0) {
throw new Error('No structured data could be extracted from the input.');
}
const extractedData = toolCalls[0].input;
if (Object.keys(sanitizedNameMap).length > 0 && extractedData && typeof extractedData === 'object') {
const restoredData: Record<string, unknown> = {};
for (const [key, value] of Object.entries(extractedData)) {
const originalName = sanitizedNameMap[key] ?? key;
restoredData[originalName] = value;
}
return restoredData;
}
return extractedData;
} catch (error) {
throw new Error(`Failed to extract structured data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,78 @@
import { anthropic, createAnthropic } from '@ai-sdk/anthropic'
import { createGoogleGenerativeAI, google } from '@ai-sdk/google'
import { createOpenAI, openai } from '@ai-sdk/openai'
import { LanguageModelV2 } from '@ai-sdk/provider'
import { createAzure } from '@ai-sdk/azure'
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import { ImageModel } from 'ai'
import { AIProviderConfig, AIProviderName, AzureProviderConfig } from './types'
import { httpClient, HttpMethod } from '@activepieces/pieces-common'
type CreateAIModelParams<IsImage extends boolean = false> = {
providerId: string;
modelId: string;
engineToken: string;
apiUrl: string;
openaiResponsesModel?: boolean;
isImage?: IsImage;
}
export function createAIModel(params: CreateAIModelParams<false>): Promise<LanguageModelV2>;
export function createAIModel(params: CreateAIModelParams<true>): Promise<ImageModel>;
export async function createAIModel({
providerId,
modelId,
engineToken,
apiUrl,
openaiResponsesModel = false,
isImage,
}: CreateAIModelParams<boolean>): Promise<ImageModel | LanguageModelV2> {
const { body: config } = await httpClient.sendRequest<AIProviderConfig>({
method: HttpMethod.GET,
url: `${apiUrl}v1/ai-providers/${providerId}/config`,
headers: {
Authorization: `Bearer ${engineToken}`,
},
});
switch (providerId) {
case AIProviderName.OPENAI: {
const provider = createOpenAI({ apiKey: config.apiKey })
if (isImage) {
return provider.imageModel(modelId)
}
return (openaiResponsesModel ? provider.responses(modelId) : provider.chat(modelId))
}
case AIProviderName.ANTHROPIC: {
const provider = createAnthropic({ apiKey: config.apiKey })
if (isImage) {
throw new Error(`Provider ${providerId} does not support image models`)
}
return provider(modelId)
}
case AIProviderName.GOOGLE: {
const provider = createGoogleGenerativeAI({ apiKey: config.apiKey })
return provider(modelId)
}
case AIProviderName.AZURE: {
const { apiKey, resourceName } = config as AzureProviderConfig
const provider = createAzure({ resourceName, apiKey })
if (isImage) {
return provider.imageModel(modelId)
}
return provider.chat(modelId)
}
case AIProviderName.ACTIVEPIECES:
case AIProviderName.OPENROUTER: {
const provider = createOpenRouter({ apiKey: config.apiKey })
return provider.chat(modelId)
}
default:
throw new Error(`Provider ${providerId} is not supported`)
}
}
export const anthropicSearchTool = anthropic.tools.webSearch_20250305;
export const openaiSearchTool = openai.tools.webSearchPreview;
export const googleSearchTool = google.tools.googleSearch;

View File

@@ -0,0 +1,88 @@
import { PieceAuth, Property } from "@activepieces/pieces-framework";
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { AIProviderModel, AIProviderName, AIProviderWithoutSensitiveData } from './types';
export const aiProps = <T extends 'text' | 'image'>({ modelType, allowedProviders }: AIPropsParams<T>) => ({
provider: Property.Dropdown<string, true>({
auth: PieceAuth.None(),
displayName: 'Provider',
required: true,
refreshers: [],
options: async (_, ctx) => {
const { body: supportedProviders } = await httpClient.sendRequest<AIProviderWithoutSensitiveData[]>({
method: HttpMethod.GET,
url: `${ctx.server.apiUrl}v1/ai-providers`,
headers: {
Authorization: `Bearer ${ctx.server.token}`,
},
});
const configured = supportedProviders.filter(supportedProvider => supportedProvider.configured);
if (configured.length === 0) {
return {
disabled: true,
options: [],
placeholder: 'No AI providers configured by the admin.',
};
}
return {
placeholder: 'Select AI Provider',
disabled: false,
options: configured.map(supportedProvider => ({
label: supportedProvider.name,
value: supportedProvider.id,
})).filter(provider => allowedProviders ? allowedProviders.includes(provider.value as AIProviderName) : true),
};
},
}),
model: Property.Dropdown({
auth: PieceAuth.None(),
displayName: 'Model',
required: true,
defaultValue: 'gpt-4o',
refreshers: ['provider'],
options: async (propsValue, ctx) => {
const provider = propsValue['provider'] as string;
if (isNil(provider)) {
return {
disabled: true,
options: [],
placeholder: 'Select AI Provider',
};
}
const { body: allModels } = await httpClient.sendRequest<AIProviderModel[]>({
method: HttpMethod.GET,
url: `${ctx.server.apiUrl}v1/ai-providers/${provider}/models`,
headers: {
Authorization: `Bearer ${ctx.server.token}`,
},
});
const models = allModels
.filter(model => model.type === modelType)
.filter(model => {
if (provider !== AIProviderName.ACTIVEPIECES) {
return true;
}
return Object.values([AIProviderName.OPENAI, AIProviderName.ANTHROPIC, AIProviderName.GOOGLE]).some(allowedProvider => model.id.toLowerCase().startsWith(allowedProvider.toLowerCase() + '/'));
}).sort((a, b) => a.name.localeCompare(b.name));
return {
placeholder: 'Select AI Model',
disabled: false,
options: models.map(model => ({
label: model.name,
value: model.id,
})),
};
},
}),
})
type AIPropsParams<T extends 'text' | 'image'> = {
modelType: T,
allowedProviders?: AIProviderName[]
}

View File

@@ -0,0 +1,119 @@
import { BaseModelSchema, DiscriminatedUnion } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export const AnthropicProviderConfig = Type.Object({
apiKey: Type.String(),
})
export type AnthropicProviderConfig = Static<typeof AnthropicProviderConfig>
export const AzureProviderConfig = Type.Object({
apiKey: Type.String(),
resourceName: Type.String(),
})
export type AzureProviderConfig = Static<typeof AzureProviderConfig>
export const GoogleProviderConfig = Type.Object({
apiKey: Type.String(),
})
export type GoogleProviderConfig = Static<typeof GoogleProviderConfig>
export const OpenAIProviderConfig = Type.Object({
apiKey: Type.String(),
})
export type OpenAIProviderConfig = Static<typeof OpenAIProviderConfig>
export const OpenRouterProviderConfig = Type.Object({
apiKey: Type.String(),
})
export type OpenRouterProviderConfig = Static<typeof OpenRouterProviderConfig>
export const AIProviderConfig = Type.Union([
AnthropicProviderConfig,
AzureProviderConfig,
GoogleProviderConfig,
OpenAIProviderConfig,
OpenRouterProviderConfig,
])
export type AIProviderConfig = Static<typeof AIProviderConfig>
export enum
AIProviderName {
OPENAI = 'openai',
OPENROUTER = 'openrouter',
ANTHROPIC = 'anthropic',
AZURE = 'azure',
GOOGLE = 'google',
ACTIVEPIECES = 'activepieces',
}
const ProviderConfigUnion = DiscriminatedUnion('provider', [
Type.Object({
provider: Type.Literal(AIProviderName.OPENAI),
config: OpenAIProviderConfig,
}),
Type.Object({
provider: Type.Literal(AIProviderName.OPENROUTER),
config: OpenRouterProviderConfig,
}),
Type.Object({
provider: Type.Literal(AIProviderName.ANTHROPIC),
config: AnthropicProviderConfig,
}),
Type.Object({
provider: Type.Literal(AIProviderName.AZURE),
config: AzureProviderConfig,
}),
Type.Object({
provider: Type.Literal(AIProviderName.GOOGLE),
config: GoogleProviderConfig,
}),
Type.Object({
provider: Type.Literal(AIProviderName.ACTIVEPIECES),
config: OpenRouterProviderConfig,
}),
]);
export const AIProvider = Type.Intersect([
Type.Object({ ...BaseModelSchema }),
ProviderConfigUnion,
Type.Object({
displayName: Type.String({ minLength: 1 }),
platformId: Type.String(),
}),
]);
export type AIProvider = Static<typeof AIProvider>
export const AIProviderWithoutSensitiveData = Type.Object({
id: Type.String(),
name: Type.String(),
configured: Type.Boolean(),
})
export type AIProviderWithoutSensitiveData = Static<typeof AIProviderWithoutSensitiveData>
export enum AIProviderModelType {
IMAGE = 'image',
TEXT = 'text',
}
export const AIProviderModel = Type.Object({
id: Type.String(),
name: Type.String(),
type: Type.Enum(AIProviderModelType),
})
export type AIProviderModel = Static<typeof AIProviderModel>
export const CreateAIProviderRequest = ProviderConfigUnion
export type CreateAIProviderRequest = Static<typeof CreateAIProviderRequest>
export const AIErrorResponse = Type.Object({
error: Type.Object({
message: Type.String(),
type: Type.String(),
code: Type.String(),
}),
})
export type AIErrorResponse = Static<typeof AIErrorResponse>