diff --git a/packages/pieces/community/ai/src/lib/actions/agents/run-agent.ts b/packages/pieces/community/ai/src/lib/actions/agents/run-agent.ts index 6da1d09834f..872bd36538d 100644 --- a/packages/pieces/community/ai/src/lib/actions/agents/run-agent.ts +++ b/packages/pieces/community/ai/src/lib/actions/agents/run-agent.ts @@ -13,11 +13,11 @@ import { AgentTool, TASK_COMPLETION_TOOL_NAME, AIProviderName, + AgentProviderModel, } from '@activepieces/shared'; import { hasToolCall, stepCountIs, streamText } from 'ai'; import { agentOutputBuilder } from './agent-output-builder'; import { createAIModel } from '../../common/ai-sdk'; -import { aiProps } from '../../common/props'; import { inspect } from 'util'; import { agentUtils } from './utils'; import { constructAgentTools } from './tools'; @@ -68,8 +68,10 @@ export const runAgent = createAction({ 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.AI_PROVIDER_MODEL]: Property.Object({ + displayName: 'AI Model', + required: true, + }), [AgentPieceProps.AGENT_TOOLS]: Property.Array({ displayName: 'Agent Tools', required: false, @@ -102,10 +104,12 @@ export const runAgent = createAction({ }), }, async run(context) { - const { prompt, maxSteps, model: modelId, provider } = context.propsValue; + const { prompt, maxSteps, aiProviderModel } = context.propsValue; + const agentProviderModel = aiProviderModel as AgentProviderModel + const model = await createAIModel({ - modelId, - provider: provider as AIProviderName, + modelId: agentProviderModel.model, + provider: agentProviderModel.provider as AIProviderName, engineToken: context.server.token, apiUrl: context.server.apiUrl, projectId: context.project.id, @@ -199,12 +203,13 @@ export const runAgent = createAction({ } } - const { status } = outputBuilder.build(); - if (errors.length > 0 || status === AgentTaskStatus.IN_PROGRESS) { + if (errors.length > 0) { const errorSummary = errors.map(e => `${e.type}: ${e.message}`).join('\n'); outputBuilder.addMarkdown(`\n\n**Errors encountered:**\n${errorSummary}`); outputBuilder.fail({ message: 'Agent completed with errors' }); await context.output.update({ data: outputBuilder.build() }); + } else { + outputBuilder.setStatus(AgentTaskStatus.COMPLETED) } } catch (error) { 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 92ef1bd41d3..b78bd500c3a 100644 --- a/packages/pieces/community/ai/src/lib/actions/agents/utils.ts +++ b/packages/pieces/community/ai/src/lib/actions/agents/utils.ts @@ -55,6 +55,7 @@ export const agentUtils = { **Core Objective**: - Help the user achieve their goal as quickly, accurately, and thoroughly as possible. - Always prioritize user satisfaction by providing clear, concise, and relevant responses. + - Always make sure when u are asked a direct simple question you replay to it in simple clear and consize text response. **Reasoning and Thinking Guidelines**: - Think step-by-step before taking any action. Use chain-of-thought reasoning: First, understand the user's query fully. Then, break it down into sub-tasks. Evaluate what information or actions are needed. Finally, decide on the next steps. diff --git a/packages/pieces/community/ai/src/lib/common/props.ts b/packages/pieces/community/ai/src/lib/common/props.ts index 5f05cc25f78..c781cac614b 100644 --- a/packages/pieces/community/ai/src/lib/common/props.ts +++ b/packages/pieces/community/ai/src/lib/common/props.ts @@ -7,16 +7,6 @@ import { AIProviderWithoutSensitiveData, } from '@activepieces/shared'; -type Provider = - | 'activepieces' - | 'openai' - | 'anthropic' - | 'google' - | 'openrouter' - | 'cloudflare-gateway' - | 'custom' - | 'azure'; - type AIModelType = 'text' | 'image'; type AIPropsParams = { @@ -24,44 +14,6 @@ type AIPropsParams = { allowedProviders?: AIProviderName[]; }; -const RESTRICTED_PROVIDER_MODELS: Partial> = { - openai: [ - 'gpt-5.2', - 'gpt-5.1', - 'gpt-5-mini', - ], - anthropic: [ - 'claude-sonnet-4-5-20250929', - 'claude-opus-4-5-20251101', - 'claude-haiku-4-5-20251001', - ], - google: [ - 'gemini-3-pro-preview', - 'gemini-3-flash-preview', - 'gemini-2.5-flash-preview-09-2025', - 'gemini-2.5-flash-lite-preview-09-2025', - ], -}; - -function getAllowedModelsForProvider( - provider: Provider, - allModels: AIProviderModel[], - modelType: AIModelType -): AIProviderModel[] { - const restrictedModels = RESTRICTED_PROVIDER_MODELS[provider]; - - return allModels - .filter(model => model.type === modelType) - .filter(model => { - if (isNil(restrictedModels)) { - return true; - } - - return restrictedModels.includes(model.id); - }) - .sort((a, b) => a.name.localeCompare(b.name)); -} - export const aiProps = ({ modelType, allowedProviders, @@ -104,7 +56,7 @@ export const aiProps = ({ required: true, refreshers: ['provider'], options: async (propsValue, ctx) => { - const provider = propsValue['provider'] as Provider | undefined; + const provider = propsValue['provider'] as string if (isNil(provider)) { return { @@ -123,16 +75,10 @@ export const aiProps = ({ }, }); - const models = getAllowedModelsForProvider( - provider, - allModels, - modelType - ); - return { placeholder: 'Select AI Model', disabled: false, - options: models.map(model => ({ + options: allModels.filter(model => model.type === modelType).map(model => ({ label: model.name, value: model.id, })), diff --git a/packages/pieces/community/http/package.json b/packages/pieces/community/http/package.json index b18431ff219..75108a7bab3 100644 --- a/packages/pieces/community/http/package.json +++ b/packages/pieces/community/http/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-http", - "version": "0.10.0", + "version": "0.11.0", "dependencies": { "https-proxy-agent": "7.0.4" } diff --git a/packages/pieces/community/http/src/lib/actions/send-http-request-action.ts b/packages/pieces/community/http/src/lib/actions/send-http-request-action.ts index ab1dbf3ff13..83dbd44bbab 100644 --- a/packages/pieces/community/http/src/lib/actions/send-http-request-action.ts +++ b/packages/pieces/community/http/src/lib/actions/send-http-request-action.ts @@ -253,11 +253,7 @@ export const httpSendRequestAction = createAction({ { label: 'Do not continue (stop the flow)', value: 'continue_none' }, ], }, - }), - stopFlow: Property.Checkbox({ - displayName: 'Stop the flow on Failure ?', - required: false, - }), + }) }, errorHandlingOptions: { continueOnFailure: { hide: true, defaultValue: false }, @@ -277,7 +273,6 @@ export const httpSendRequestAction = createAction({ use_proxy, authType, authFields, - stopFlow, } = context.propsValue; assertNotNullOrUndefined(method, 'Method'); @@ -381,10 +376,6 @@ export const httpSendRequestAction = createAction({ } catch (error) { attempts++; - if (stopFlow) { - throw error; - } - switch (failureMode) { case 'retry_all': { if (attempts < 3) continue; diff --git a/packages/react-ui/src/app/builder/step-settings/agent-settings/index.tsx b/packages/react-ui/src/app/builder/step-settings/agent-settings/index.tsx index e22fb1f7566..478d663b61d 100644 --- a/packages/react-ui/src/app/builder/step-settings/agent-settings/index.tsx +++ b/packages/react-ui/src/app/builder/step-settings/agent-settings/index.tsx @@ -3,9 +3,11 @@ import { useFormContext } from 'react-hook-form'; import { AgentTools } from '@/app/builder/step-settings/agent-settings/agent-tools'; import { FormField } from '@/components/ui/form'; import { Skeleton } from '@/components/ui/skeleton'; +import { AIModelSelector } from '@/features/agents/ai-model'; import { AgentStructuredOutput } from '@/features/agents/structured-output'; import { AgentPieceProps, + AgentProviderModel, isNil, PieceAction, PieceActionSettings, @@ -108,6 +110,18 @@ const selectAgentFormComponentForProperty = ( /> ); } + case AgentPieceProps.AI_PROVIDER_MODEL: { + const provider = (field.value as AgentProviderModel).provider; + const model = (field.value as AgentProviderModel).model; + return ( + + ); + } default: { return selectGenericFormComponentForProperty(params); } diff --git a/packages/react-ui/src/app/builder/step-settings/agent-settings/predefined-inputs-form.tsx b/packages/react-ui/src/app/builder/step-settings/agent-settings/predefined-inputs-form.tsx index fce8940c307..393d29fafc4 100644 --- a/packages/react-ui/src/app/builder/step-settings/agent-settings/predefined-inputs-form.tsx +++ b/packages/react-ui/src/app/builder/step-settings/agent-settings/predefined-inputs-form.tsx @@ -4,6 +4,7 @@ import { t } from 'i18next'; import { useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; +import { ApMarkdown } from '@/components/custom/markdown'; import { Form, FormField } from '@/components/ui/form'; import { ScrollArea } from '@/components/ui/scroll-area'; import { @@ -16,7 +17,7 @@ import { import { ConnectionDropdown } from '@/features/agents/agent-tools/piece-tool-dialog/connection-select'; import { usePieceToolsDialogStore } from '@/features/agents/agent-tools/stores/pieces-tools'; import { piecesHooks } from '@/features/pieces/lib/pieces-hooks'; -import { PieceProperty } from '@activepieces/pieces-framework'; +import { PieceProperty, PropertyType } from '@activepieces/pieces-framework'; import { FieldControlMode, isNil, @@ -24,6 +25,7 @@ import { } from '@activepieces/shared'; import { selectGenericFormComponentForProperty } from '../../piece-properties/properties-utils'; + const createPredefinedInputsFormSchema = (requireAuth: boolean) => Type.Object( requireAuth @@ -36,9 +38,11 @@ const createPredefinedInputsFormSchema = (requireAuth: boolean) => ...(requireAuth && { required: ['auth'] }), }, ); + type PredefinedInputsFormValues = Static< ReturnType >; + export const PredefinedInputsForm = () => { const { predefinedInputs, @@ -48,7 +52,7 @@ export const PredefinedInputsForm = () => { } = usePieceToolsDialogStore(); const { pieces } = piecesHooks.usePieces({}); const selectedPiece = pieces?.find((p) => p.name === piece?.pieceName); - const requireAuth = selectedAction?.requireAuth ?? false; + const requireAuth = selectedAction?.requireAuth ?? true; const formSchema = useMemo( () => createPredefinedInputsFormSchema(requireAuth), [requireAuth], @@ -150,7 +154,7 @@ export const PredefinedInputsForm = () => { form.setValue(propertyName, undefined, { shouldDirty: false }); } }; - const pieceHasAuth = requireAuth; + const pieceHasAuth = requireAuth && selectedPiece?.auth; return (
@@ -185,6 +189,19 @@ export const PredefinedInputsForm = () => { {Object.keys(properties).length > 0 && (
{Object.entries(properties).map(([propertyName, property]) => { + const isMarkdown = property.type === PropertyType.MARKDOWN; + + if (isMarkdown) { + return ( + + ); + } + const mode = getModeForProperty(propertyName); const showInput = mode === FieldControlMode.CHOOSE_YOURSELF; return ( diff --git a/packages/react-ui/src/app/routes/platform/setup/ai/index.tsx b/packages/react-ui/src/app/routes/platform/setup/ai/index.tsx index f801cf8f156..c342574f006 100644 --- a/packages/react-ui/src/app/routes/platform/setup/ai/index.tsx +++ b/packages/react-ui/src/app/routes/platform/setup/ai/index.tsx @@ -5,12 +5,15 @@ import { DashboardPageHeader } from '@/app/components/dashboard-page-header'; import { aiProviderApi } from '@/features/platform-admin/lib/ai-provider-api'; import { flagsHooks } from '@/hooks/flags-hooks'; import { userHooks } from '@/hooks/user-hooks'; -import { PlatformRole, ApFlagId } from '@activepieces/shared'; +import { + PlatformRole, + ApFlagId, + SUPPORTED_AI_PROVIDERS, +} from '@activepieces/shared'; import LockedFeatureGuard from '../../../../components/locked-feature-guard'; import { AIProviderCard } from './universal-pieces/ai-provider-card'; -import { SUPPORTED_AI_PROVIDERS } from './universal-pieces/supported-ai-providers'; export default function AIProvidersPage() { const { data: providers, refetch } = useQuery({ diff --git a/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/ai-provider-card.tsx b/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/ai-provider-card.tsx index 6dd657e7637..c31f7f3eca5 100644 --- a/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/ai-provider-card.tsx +++ b/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/ai-provider-card.tsx @@ -3,9 +3,11 @@ import { Pencil, Trash } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; -import { AIProviderWithoutSensitiveData } from '@activepieces/shared'; +import { + AiProviderInfo, + AIProviderWithoutSensitiveData, +} from '@activepieces/shared'; -import { AiProviderInfo } from './supported-ai-providers'; import { UpsertAIProviderDialog } from './upsert-provider-dialog'; type AIProviderCardProps = { diff --git a/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/supported-ai-providers.ts b/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/supported-ai-providers.ts deleted file mode 100644 index 5a6366dcccd..00000000000 --- a/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/supported-ai-providers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { t } from 'i18next'; - -import { AIProviderName } from '@activepieces/shared'; - -export const SUPPORTED_AI_PROVIDERS: AiProviderInfo[] = [ - { - provider: AIProviderName.OPENAI, - name: 'OpenAI', - markdown: t(`Follow these instructions to get your OpenAI API Key: - -1. Visit the following website: https://platform.openai.com/account/api-keys. -2. Once on the website, locate and click on the option to obtain your OpenAI API Key. - -It is strongly recommended that you add your credit card information to your OpenAI account and upgrade to the paid plan **before** generating the API Key. This will help you prevent 429 errors. -`), - logoUrl: 'https://cdn.activepieces.com/pieces/openai.png', - }, - { - provider: AIProviderName.ANTHROPIC, - name: 'Anthropic', - markdown: t(`Follow these instructions to get your Claude API Key: - -1. Visit the following website: https://console.anthropic.com/settings/keys. -2. Once on the website, locate and click on the option to obtain your Claude API Key. -`), - logoUrl: 'https://cdn.activepieces.com/pieces/claude.png', - }, - { - provider: AIProviderName.GOOGLE, - name: 'Google Gemini', - markdown: t(`Follow these instructions to get your Google API Key: -1. Visit the following website: https://console.cloud.google.com/apis/credentials. -2. Once on the website, locate and click on the option to obtain your Google API Key. -`), - logoUrl: 'https://cdn.activepieces.com/pieces/google-gemini.png', - }, - { - provider: AIProviderName.AZURE, - name: 'Azure', - logoUrl: 'https://cdn.activepieces.com/pieces/azure-openai.png', - markdown: t( - 'Use the Azure Portal to browse to your OpenAI resource and retrieve an API key and resource name.', - ), - }, - { - provider: AIProviderName.OPENROUTER, - name: 'OpenRouter', - logoUrl: 'https://cdn.activepieces.com/pieces/openrouter.jpg', - markdown: t(`Follow these instructions to get your OpenRouter API Key: -1. Visit the following website: https://openrouter.ai/settings/keys. -2. Once on the website, locate and click on the option to obtain your OpenRouter API Key.`), - }, - { - provider: AIProviderName.CLOUDFLARE_GATEWAY, - name: 'Cloudflare AI Gateway', - logoUrl: 'https://cdn.activepieces.com/pieces/cloudflare-gateway.png', - markdown: - t(`Follow these instructions to get your Cloudflare AI Gateway API Key: -1. Visit the following website: https://developers.cloudflare.com/ai-gateway/get-started/. -2. Once on the website, follow the instructions to get your account id, gateway id and create an API Key. -3. After creating the gateway, make sure to enable the Authenticated Gateway Option in your settings. -4. For each provider you are using, include your keys in the Provider Keys tab.`), - }, - { - provider: AIProviderName.CUSTOM, - name: 'OpenAI Compatible', - logoUrl: 'https://cdn.activepieces.com/pieces/openai-compatible.png', - markdown: - t(`Follow these instructions to get your OpenAI Compatible API Key: -1. Set the base url to your proxy url. -2. In the api key header, set the value of your auth header name. -3. In the api key, set your auth header value (full value including the Bearer if any).`), - }, -]; - -export type AiProviderInfo = { - provider: AIProviderName; - name: string; - markdown: string; - logoUrl: string; -}; diff --git a/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/upsert-provider-dialog.tsx b/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/upsert-provider-dialog.tsx index 17a94420f2b..f094959e7e8 100644 --- a/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/upsert-provider-dialog.tsx +++ b/packages/react-ui/src/app/routes/platform/setup/ai/universal-pieces/upsert-provider-dialog.tsx @@ -41,12 +41,12 @@ import { OpenAICompatibleProviderConfig, OpenAIProviderAuthConfig, OpenAIProviderConfig, + SUPPORTED_AI_PROVIDERS, UpdateAIProviderRequest, } from '@activepieces/shared'; import { ApMarkdown } from '../../../../../../components/custom/markdown'; -import { SUPPORTED_AI_PROVIDERS } from './supported-ai-providers'; import { UpsertProviderConfigForm } from './upsert-provider-config-form'; type UpsertAIProviderDialogProps = { diff --git a/packages/react-ui/src/features/agents/ai-model/hooks.ts b/packages/react-ui/src/features/agents/ai-model/hooks.ts new file mode 100644 index 00000000000..5ec8ce67b95 --- /dev/null +++ b/packages/react-ui/src/features/agents/ai-model/hooks.ts @@ -0,0 +1,81 @@ +import { useQuery } from '@tanstack/react-query'; + +import { aiProviderApi } from '@/features/platform-admin/lib/ai-provider-api'; +import { AIProviderModel, isNil } from '@activepieces/shared'; + +type Provider = + | 'activepieces' + | 'openai' + | 'anthropic' + | 'google' + | 'openrouter' + | 'cloudflare-gateway' + | 'custom' + | 'azure'; + +type AIModelType = 'text' | 'image'; + +const OPENAI_MODELS = ['gpt-5.2', 'gpt-5.1', 'gpt-5-mini'] as const; + +const ANTHROPIC_MODELS = [ + 'claude-sonnet-4-5-20250929', + 'claude-opus-4-5-20251101', + 'claude-haiku-4-5-20251001', +] as const; + +const GOOGLE_MODELS = [ + 'gemini-3-pro-preview', + 'gemini-3-flash-preview', + 'gemini-2.5-flash-preview-09-2025', + 'gemini-2.5-flash-lite-preview-09-2025', +] as const; + +const ALLOWED_MODELS_BY_PROVIDER: Partial> = + { + openai: OPENAI_MODELS, + anthropic: ANTHROPIC_MODELS, + google: GOOGLE_MODELS, + activepieces: [...OPENAI_MODELS, ...ANTHROPIC_MODELS, ...GOOGLE_MODELS], + }; + +function getAllowedModelsForProvider( + provider: Provider, + allModels: AIProviderModel[], + modelType: AIModelType, +): AIProviderModel[] { + const allowedIds = ALLOWED_MODELS_BY_PROVIDER[provider]; + + return allModels + .filter((model) => model.type === modelType) + .filter((model) => { + if (isNil(allowedIds)) { + return true; + } + + return allowedIds.includes(model.id); + }) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +export const aiModelHooks = { + useListProviders: () => { + return useQuery({ + queryKey: ['ai-providers'], + queryFn: () => aiProviderApi.list(), + }); + }, + + useGetModelsForProvider: (provider?: Provider) => { + return useQuery({ + queryKey: ['ai-models', provider], + enabled: !!provider, + queryFn: async () => { + if (isNil(provider)) return []; + + const allModels = await aiProviderApi.listModelsForProvider(provider); + + return getAllowedModelsForProvider(provider, allModels, 'text'); + }, + }); + }, +}; diff --git a/packages/react-ui/src/features/agents/ai-model/index.tsx b/packages/react-ui/src/features/agents/ai-model/index.tsx new file mode 100644 index 00000000000..4f485011c11 --- /dev/null +++ b/packages/react-ui/src/features/agents/ai-model/index.tsx @@ -0,0 +1,205 @@ +import { t } from 'i18next'; +import * as React from 'react'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { AIProviderName, SUPPORTED_AI_PROVIDERS } from '@activepieces/shared'; + +import { aiModelHooks } from './hooks'; + +type AIModelSelectorProps = { + defaultProvider?: AIProviderName; + defaultModel?: string; + disabled: boolean; + onChange: (value: { provider?: string; model?: string }) => void; +}; + +export function AIModelSelector({ + defaultProvider, + defaultModel, + disabled, + onChange, +}: AIModelSelectorProps) { + const { data: providers = [], isLoading: providersLoading } = + aiModelHooks.useListProviders(); + + const [selectedProvider, setSelectedProvider] = React.useState< + AIProviderName | undefined + >(defaultProvider); + + const [selectedModel, setSelectedModel] = React.useState( + defaultModel, + ); + + const getProviderLogo = (providerName: string) => { + return SUPPORTED_AI_PROVIDERS.find((p) => p.provider === providerName) + ?.logoUrl; + }; + + const { data: models = [], isLoading: modelsLoading } = + aiModelHooks.useGetModelsForProvider(selectedProvider); + + const activepiecesProvider = React.useMemo( + () => providers.find((p) => p.provider === AIProviderName.ACTIVEPIECES), + [providers], + ); + + React.useEffect(() => { + if (!selectedProvider && !providersLoading && providers.length > 0) { + const preferred = + activepiecesProvider?.provider || providers[0]?.provider; + if (preferred) { + setSelectedProvider(preferred as AIProviderName); + } + } + }, [providers, providersLoading, selectedProvider, activepiecesProvider]); + + React.useEffect(() => { + if ( + selectedProvider && + models.length > 0 && + !selectedModel && + !modelsLoading + ) { + const firstModel = models[0].id; + setSelectedModel(firstModel); + onChange({ provider: selectedProvider, model: firstModel }); + } + }, [models, modelsLoading, selectedProvider, selectedModel, onChange]); + + React.useEffect(() => { + if ( + selectedModel && + models.length > 0 && + !models.some((m) => m.id === selectedModel) + ) { + const fallback = models[0]?.id; + setSelectedModel(fallback); + onChange({ provider: selectedProvider, model: fallback }); + } + }, [models, selectedModel, selectedProvider, onChange]); + + const isLoading = providersLoading || modelsLoading; + const hasProviders = providers.length > 0; + const hasModels = models.length > 0; + + return ( +
+

{t('AI Model *')}

+ +
+ + +
+ + +
+
+ ); +} diff --git a/packages/react-ui/src/features/platform-admin/lib/ai-provider-api.ts b/packages/react-ui/src/features/platform-admin/lib/ai-provider-api.ts index ca6d7475e8f..11b8eb7bb7b 100644 --- a/packages/react-ui/src/features/platform-admin/lib/ai-provider-api.ts +++ b/packages/react-ui/src/features/platform-admin/lib/ai-provider-api.ts @@ -1,5 +1,6 @@ import { api } from '@/lib/api'; import { + AIProviderModel, AIProviderWithoutSensitiveData, CreateAIProviderRequest, UpdateAIProviderRequest, @@ -9,6 +10,9 @@ export const aiProviderApi = { list() { return api.get('/v1/ai-providers'); }, + listModelsForProvider(provider: string) { + return api.get(`/v1/ai-providers/${provider}/models`); + }, upsert(request: CreateAIProviderRequest): Promise { return api.post('/v1/ai-providers', request); }, diff --git a/packages/server/api/src/app/ai/ai-provider-controller.ts b/packages/server/api/src/app/ai/ai-provider-controller.ts index 3c67f3bd126..40ec5623314 100644 --- a/packages/server/api/src/app/ai/ai-provider-controller.ts +++ b/packages/server/api/src/app/ai/ai-provider-controller.ts @@ -51,7 +51,7 @@ const GetAIProviderConfig = { const ListModels = { config: { - security: securityAccess.engine(), + security: securityAccess.publicPlatform([PrincipalType.USER, PrincipalType.ENGINE]), }, schema: { params: Type.Object({ diff --git a/packages/server/api/src/app/flows/flow-version/migrations/index.ts b/packages/server/api/src/app/flows/flow-version/migrations/index.ts index 6b34d0a7f56..d8ce5e4c153 100644 --- a/packages/server/api/src/app/flows/flow-version/migrations/index.ts +++ b/packages/server/api/src/app/flows/flow-version/migrations/index.ts @@ -5,6 +5,7 @@ import { migrateV10AiPiecesProviderId } from './migrate-v10-ai-pieces-provider-i import { migrateV11TablesToV2 } from './migrate-v11-tables-to-v2' import { migrateV12FixPieceVersion } from './migrate-v12-fix-piece-version' import { migrateV13AddNotes } from './migrate-v13-add-notes' +import { migrateV13AgentProviderModel } from './migrate-v13-agent-provider-model' import { migrateAgentPieceV2 } from './migrate-v2-agent-piece' import { migrateAgentPieceV3 } from './migrate-v3-agent-piece' import { migrateAgentPieceV4 } from './migrate-v4-agent-piece' @@ -34,6 +35,7 @@ const migrations: Migration[] = [ migrateV11TablesToV2, migrateV12FixPieceVersion, migrateV13AddNotes, + migrateV13AgentProviderModel, ] as const export const flowMigrations = { diff --git a/packages/server/api/src/app/flows/flow-version/migrations/migrate-v13-agent-provider-model.ts b/packages/server/api/src/app/flows/flow-version/migrations/migrate-v13-agent-provider-model.ts new file mode 100644 index 00000000000..665c60d9bad --- /dev/null +++ b/packages/server/api/src/app/flows/flow-version/migrations/migrate-v13-agent-provider-model.ts @@ -0,0 +1,42 @@ +import { + AgentPieceProps, + FlowActionType, + flowStructureUtil, + FlowVersion, +} from '@activepieces/shared' +import { Migration } from '.' + +export const migrateV13AgentProviderModel: Migration = { + targetSchemaVersion: '14', + migrate: async (flowVersion: FlowVersion): Promise => { + const newVersion = flowStructureUtil.transferFlow(flowVersion, (step) => { + if (step.type === FlowActionType.PIECE && step.settings.pieceName === '@activepieces/piece-ai') { + const actionName = step.settings.actionName + const input = step.settings?.input as Record + + if (actionName === 'run_agent') { + const provider = input['provider'] as string + const model = input['model'] as string + + return { + ...step, + settings: { + ...step.settings, + input: { + ...input, + [AgentPieceProps.AI_PROVIDER_MODEL]: { provider, model }, + }, + }, + } + } + return step + } + return step + }) + + return { + ...newVersion, + schemaVersion: '15', + } + }, +} \ No newline at end of file diff --git a/packages/server/api/src/app/flows/flow-version/migrations/migrate-v7-agents-to-flow-version.ts b/packages/server/api/src/app/flows/flow-version/migrations/migrate-v7-agents-to-flow-version.ts index 81a4584175c..80755af920e 100644 --- a/packages/server/api/src/app/flows/flow-version/migrations/migrate-v7-agents-to-flow-version.ts +++ b/packages/server/api/src/app/flows/flow-version/migrations/migrate-v7-agents-to-flow-version.ts @@ -91,11 +91,10 @@ export const moveAgentsToFlowVerion: Migration = { pieceVersion: '0.3.0', input: { [AgentPieceProps.PROMPT]: `${agent?.systemPrompt}, ${prompt}`, - [AgentPieceProps.AI_MODEL]: 'openai-gpt-4o', + 'model': 'openai-gpt-4o', [AgentPieceProps.MAX_STEPS]: agent?.maxSteps, [AgentPieceProps.STRUCTURED_OUTPUT]: typeof agent?.outputFields === 'string' ? JSON.parse(agent?.outputFields as string || '[]') : [], - [AgentPieceProps.AGENT_TOOLS]: tools, - }, + [AgentPieceProps.AGENT_TOOLS]: tools }, } } return step diff --git a/packages/server/api/src/app/flows/flow/flow.service.ts b/packages/server/api/src/app/flows/flow/flow.service.ts index b6c622c38be..1fdbe718050 100644 --- a/packages/server/api/src/app/flows/flow/flow.service.ts +++ b/packages/server/api/src/app/flows/flow/flow.service.ts @@ -338,7 +338,7 @@ export const flowService = (log: FastifyBaseLogger) => ({ throw new ActivepiecesError({ code: ErrorCode.FLOW_OPERATION_IN_PROGRESS, params: { - message: `This flow is getting deleted.`, + message: 'This flow is getting deleted.', }, }) } diff --git a/packages/shared/src/lib/agents/index.ts b/packages/shared/src/lib/agents/index.ts index c2412fafb3c..c6e70585de3 100644 --- a/packages/shared/src/lib/agents/index.ts +++ b/packages/shared/src/lib/agents/index.ts @@ -1,4 +1,5 @@ import { Static, Type } from '@sinclair/typebox' +import { AIProviderName } from '../ai-providers' import { DiscriminatedUnion, Nullable } from '../common' export * from './tools' @@ -54,8 +55,12 @@ export enum AgentPieceProps { STRUCTURED_OUTPUT = 'structuredOutput', PROMPT = 'prompt', MAX_STEPS = 'maxSteps', - AI_PROVIDER = 'provider', - AI_MODEL = 'model', + AI_PROVIDER_MODEL = 'aiProviderModel', +} + +export type AgentProviderModel = { + provider: AIProviderName + model: string } export const MarkdownContentBlock = Type.Object({ diff --git a/packages/shared/src/lib/ai-providers/index.ts b/packages/shared/src/lib/ai-providers/index.ts index 0c35b469198..ac624686fd5 100644 --- a/packages/shared/src/lib/ai-providers/index.ts +++ b/packages/shared/src/lib/ai-providers/index.ts @@ -1,6 +1,96 @@ import { Static, Type } from '@sinclair/typebox' +import { t } from 'i18next' import { BaseModelSchema, DiscriminatedUnion } from '../common/base-model' +export enum AIProviderName { + OPENAI = 'openai', + OPENROUTER = 'openrouter', + ANTHROPIC = 'anthropic', + AZURE = 'azure', + GOOGLE = 'google', + ACTIVEPIECES = 'activepieces', + CLOUDFLARE_GATEWAY = 'cloudflare-gateway', + CUSTOM = 'custom', +} + +export const SUPPORTED_AI_PROVIDERS: AiProviderInfo[] = [ + { + provider: AIProviderName.OPENAI, + name: 'OpenAI', + markdown: t(`Follow these instructions to get your OpenAI API Key: + +1. Visit the following website: https://platform.openai.com/account/api-keys. +2. Once on the website, locate and click on the option to obtain your OpenAI API Key. + +It is strongly recommended that you add your credit card information to your OpenAI account and upgrade to the paid plan **before** generating the API Key. This will help you prevent 429 errors. +`), + logoUrl: 'https://cdn.activepieces.com/pieces/openai.png', + }, + { + provider: AIProviderName.ANTHROPIC, + name: 'Anthropic', + markdown: t(`Follow these instructions to get your Claude API Key: + +1. Visit the following website: https://console.anthropic.com/settings/keys. +2. Once on the website, locate and click on the option to obtain your Claude API Key. +`), + logoUrl: 'https://cdn.activepieces.com/pieces/claude.png', + }, + { + provider: AIProviderName.GOOGLE, + name: 'Google Gemini', + markdown: t(`Follow these instructions to get your Google API Key: +1. Visit the following website: https://console.cloud.google.com/apis/credentials. +2. Once on the website, locate and click on the option to obtain your Google API Key. +`), + logoUrl: 'https://cdn.activepieces.com/pieces/google-gemini.png', + }, + { + provider: AIProviderName.AZURE, + name: 'Azure', + logoUrl: 'https://cdn.activepieces.com/pieces/azure-openai.png', + markdown: t( + 'Use the Azure Portal to browse to your OpenAI resource and retrieve an API key and resource name.', + ), + }, + { + provider: AIProviderName.OPENROUTER, + name: 'OpenRouter', + logoUrl: 'https://cdn.activepieces.com/pieces/openrouter.jpg', + markdown: t(`Follow these instructions to get your OpenRouter API Key: +1. Visit the following website: https://openrouter.ai/settings/keys. +2. Once on the website, locate and click on the option to obtain your OpenRouter API Key.`), + }, + { + provider: AIProviderName.CLOUDFLARE_GATEWAY, + name: 'Cloudflare AI Gateway', + logoUrl: 'https://cdn.activepieces.com/pieces/cloudflare-gateway.png', + markdown: + t(`Follow these instructions to get your Cloudflare AI Gateway API Key: +1. Visit the following website: https://developers.cloudflare.com/ai-gateway/get-started/. +2. Once on the website, follow the instructions to get your account id, gateway id and create an API Key. +3. After creating the gateway, make sure to enable the Authenticated Gateway Option in your settings. +4. For each provider you are using, include your keys in the Provider Keys tab.`), + }, + { + provider: AIProviderName.CUSTOM, + name: 'OpenAI Compatible', + logoUrl: 'https://cdn.activepieces.com/pieces/openai-compatible.png', + markdown: + t(`Follow these instructions to get your OpenAI Compatible API Key: +1. Set the base url to your proxy url. +2. In the api key header, set the value of your auth header name. +3. In the api key, set your auth header value (full value including the Bearer if any).`), + }, +] + +export type AiProviderInfo = { + provider: AIProviderName + name: string + markdown: string + logoUrl: string +} + export enum AIProviderModelType { IMAGE = 'image', TEXT = 'text', @@ -106,17 +196,6 @@ export const AIProviderConfig = Type.Union([ ]) export type AIProviderConfig = Static -export enum AIProviderName { - OPENAI = 'openai', - OPENROUTER = 'openrouter', - ANTHROPIC = 'anthropic', - AZURE = 'azure', - GOOGLE = 'google', - ACTIVEPIECES = 'activepieces', - CLOUDFLARE_GATEWAY = 'cloudflare-gateway', - CUSTOM = 'custom', -} - const ProviderConfigUnion = DiscriminatedUnion('provider', [ Type.Object({ displayName: Type.String({ minLength: 1 }), diff --git a/packages/shared/src/lib/flows/flow-version.ts b/packages/shared/src/lib/flows/flow-version.ts index 7146e065d31..7788c1f5f04 100755 --- a/packages/shared/src/lib/flows/flow-version.ts +++ b/packages/shared/src/lib/flows/flow-version.ts @@ -7,7 +7,7 @@ import { FlowTrigger } from './triggers/trigger' export type FlowVersionId = ApId -export const LATEST_FLOW_SCHEMA_VERSION = '14' +export const LATEST_FLOW_SCHEMA_VERSION = '15' export enum FlowVersionState { LOCKED = 'LOCKED',