feat(trigger): add trigger provider management and webhook handling functionality

This commit is contained in:
Harry
2025-08-28 11:46:35 +08:00
parent 7544b5ec9a
commit 87120ad4ac
28 changed files with 2056 additions and 57 deletions

View File

@ -68,6 +68,19 @@ class ToolProviderCredentialsCache(ProviderCredentialsCache):
return f"tool_credentials:tenant_id:{tenant_id}:provider:{provider}:credential_id:{credential_id}"
class TriggerProviderCredentialCache(ProviderCredentialsCache):
"""Cache for trigger provider credentials"""
def __init__(self, tenant_id: str, provider: str, credential_id: str):
super().__init__(tenant_id=tenant_id, provider=provider, credential_id=credential_id)
def _generate_cache_key(self, **kwargs) -> str:
tenant_id = kwargs["tenant_id"]
provider = kwargs["provider"]
credential_id = kwargs["credential_id"]
return f"trigger_credentials:tenant_id:{tenant_id}:provider:{provider}:credential_id:{credential_id}"
class NoOpProviderCredentialCache:
"""No-op provider credential cache"""

View File

@ -184,6 +184,10 @@ class ToolProviderID(GenericProviderID):
self.plugin_name = f"{self.provider_name}_tool"
class TriggerProviderID(GenericProviderID):
pass
class PluginDependency(BaseModel):
class Type(enum.StrEnum):
Github = PluginInstallationSource.Github.value

View File

@ -1,3 +1,4 @@
import enum
from collections.abc import Mapping, Sequence
from datetime import datetime
from enum import StrEnum
@ -13,6 +14,7 @@ from core.plugin.entities.parameters import PluginParameterOption
from core.plugin.entities.plugin import PluginDeclaration, PluginEntity
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin
from core.trigger.entities import TriggerProviderEntity
T = TypeVar("T", bound=(BaseModel | dict | list | bool | str))
@ -196,3 +198,43 @@ class PluginListResponse(BaseModel):
class PluginDynamicSelectOptionsResponse(BaseModel):
options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.")
class PluginTriggerProviderEntity(BaseModel):
provider: str
plugin_unique_identifier: str
plugin_id: str
declaration: TriggerProviderEntity
class CredentialType(enum.StrEnum):
API_KEY = "api-key"
OAUTH2 = "oauth2"
def get_name(self):
if self == CredentialType.API_KEY:
return "API KEY"
elif self == CredentialType.OAUTH2:
return "AUTH"
else:
return self.value.replace("-", " ").upper()
def is_editable(self):
return self == CredentialType.API_KEY
def is_validate_allowed(self):
return self == CredentialType.API_KEY
@classmethod
def values(cls):
return [item.value for item in cls]
@classmethod
def of(cls, credential_type: str) -> "CredentialType":
type_name = credential_type.lower()
if type_name == "api-key":
return cls.API_KEY
elif type_name == "oauth2":
return cls.OAUTH2
else:
raise ValueError(f"Invalid credential type: {credential_type}")

View File

@ -4,10 +4,10 @@ from typing import Any, Optional
from pydantic import BaseModel
from core.plugin.entities.plugin import GenericProviderID, ToolProviderID
from core.plugin.entities.plugin_daemon import PluginBasicBooleanResponse, PluginToolProviderEntity
from core.plugin.entities.plugin_daemon import CredentialType, PluginBasicBooleanResponse, PluginToolProviderEntity
from core.plugin.impl.base import BasePluginClient
from core.plugin.utils.chunk_merger import merge_blob_chunks
from core.tools.entities.tool_entities import CredentialType, ToolInvokeMessage, ToolParameter
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter
class PluginToolManager(BasePluginClient):

View File

@ -0,0 +1,68 @@
from typing import Any
from core.plugin.entities.plugin import ToolProviderID
from core.plugin.entities.plugin_daemon import PluginToolProviderEntity, PluginTriggerProviderEntity
from core.plugin.impl.base import BasePluginClient
class PluginTriggerManager(BasePluginClient):
def fetch_trigger_providers(self, tenant_id: str) -> list[PluginTriggerProviderEntity]:
"""
Fetch tool providers for the given tenant.
"""
def transformer(json_response: dict[str, Any]) -> dict:
for provider in json_response.get("data", []):
declaration = provider.get("declaration", {}) or {}
provider_name = declaration.get("identity", {}).get("name")
for tool in declaration.get("tools", []):
tool["identity"]["provider"] = provider_name
return json_response
response = self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/tools",
list[PluginToolProviderEntity],
params={"page": 1, "page_size": 256},
transformer=transformer,
)
for provider in response:
provider.declaration.identity.name = f"{provider.plugin_id}/{provider.declaration.identity.name}"
# override the provider name for each tool to plugin_id/provider_name
for tool in provider.declaration.tools:
tool.identity.provider = provider.declaration.identity.name
return response
def fetch_tool_provider(self, tenant_id: str, provider: str) -> PluginToolProviderEntity:
"""
Fetch tool provider for the given tenant and plugin.
"""
tool_provider_id = ToolProviderID(provider)
def transformer(json_response: dict[str, Any]) -> dict:
data = json_response.get("data")
if data:
for tool in data.get("declaration", {}).get("tools", []):
tool["identity"]["provider"] = tool_provider_id.provider_name
return json_response
response = self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/tool",
PluginToolProviderEntity,
params={"provider": tool_provider_id.provider_name, "plugin_id": tool_provider_id.plugin_id},
transformer=transformer,
)
response.declaration.identity.name = f"{response.plugin_id}/{response.declaration.identity.name}"
# override the provider name for each tool to plugin_id/provider_name
for tool in response.declaration.tools:
tool.identity.provider = response.declaration.identity.name
return response

View File

@ -4,7 +4,8 @@ from openai import BaseModel
from pydantic import Field
from core.app.entities.app_invoke_entities import InvokeFrom
from core.tools.entities.tool_entities import CredentialType, ToolInvokeFrom
from core.plugin.entities.plugin_daemon import CredentialType
from core.tools.entities.tool_entities import ToolInvokeFrom
class ToolRuntime(BaseModel):

View File

@ -4,11 +4,11 @@ from typing import Any
from core.entities.provider_entities import ProviderConfig
from core.helper.module_import_helper import load_single_subclass_from_source
from core.plugin.entities.plugin_daemon import CredentialType
from core.tools.__base.tool_provider import ToolProviderController
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.builtin_tool.tool import BuiltinTool
from core.tools.entities.tool_entities import (
CredentialType,
OAuthSchema,
ToolEntity,
ToolProviderEntity,

View File

@ -4,9 +4,10 @@ from typing import Any, Literal, Optional
from pydantic import BaseModel, Field, field_validator
from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.entities.plugin_daemon import CredentialType
from core.tools.__base.tool import ToolParameter
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import CredentialType, ToolProviderType
from core.tools.entities.tool_entities import ToolProviderType
class ToolApiEntity(BaseModel):

View File

@ -476,36 +476,3 @@ class ToolSelector(BaseModel):
def to_plugin_parameter(self) -> dict[str, Any]:
return self.model_dump()
class CredentialType(enum.StrEnum):
API_KEY = "api-key"
OAUTH2 = "oauth2"
def get_name(self):
if self == CredentialType.API_KEY:
return "API KEY"
elif self == CredentialType.OAUTH2:
return "AUTH"
else:
return self.value.replace("-", " ").upper()
def is_editable(self):
return self == CredentialType.API_KEY
def is_validate_allowed(self):
return self == CredentialType.API_KEY
@classmethod
def values(cls):
return [item.value for item in cls]
@classmethod
def of(cls, credential_type: str) -> "CredentialType":
type_name = credential_type.lower()
if type_name == "api-key":
return cls.API_KEY
elif type_name == "oauth2":
return cls.OAUTH2
else:
raise ValueError(f"Invalid credential type: {credential_type}")

View File

@ -37,6 +37,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom
from core.helper.module_import_helper import load_single_subclass_from_source
from core.helper.position_helper import is_filtered
from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.entities.plugin_daemon import CredentialType
from core.tools.__base.tool import Tool
from core.tools.builtin_tool.provider import BuiltinToolProviderController
from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
@ -47,7 +48,6 @@ from core.tools.entities.api_entities import ToolProviderApiEntity, ToolProvider
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import (
ApiProviderAuthType,
CredentialType,
ToolInvokeFrom,
ToolParameter,
ToolProviderType,

View File

@ -0,0 +1 @@
# Core trigger module initialization

View File

@ -0,0 +1,244 @@
from collections.abc import Mapping
from enum import StrEnum
from typing import Any, Optional, Union
from pydantic import BaseModel, Field
from core.tools.entities.common_entities import I18nObject
class TriggerParameterOption(BaseModel):
"""
The option of the trigger parameter
"""
value: str = Field(..., description="The value of the option")
label: I18nObject = Field(..., description="The label of the option")
class TriggerParameterType(StrEnum):
"""The type of the parameter"""
STRING = "string"
NUMBER = "number"
BOOLEAN = "boolean"
SELECT = "select"
FILE = "file"
FILES = "files"
MODEL_SELECTOR = "model-selector"
APP_SELECTOR = "app-selector"
OBJECT = "object"
ARRAY = "array"
DYNAMIC_SELECT = "dynamic-select"
class ParameterAutoGenerate(BaseModel):
"""Auto generation configuration for parameters"""
enabled: bool = Field(default=False, description="Whether auto generation is enabled")
template: Optional[str] = Field(default=None, description="Template for auto generation")
class ParameterTemplate(BaseModel):
"""Template configuration for parameters"""
value: str = Field(..., description="Template value")
type: str = Field(default="jinja2", description="Template type")
class TriggerParameter(BaseModel):
"""
The parameter of the trigger
"""
name: str = Field(..., description="The name of the parameter")
label: I18nObject = Field(..., description="The label presented to the user")
type: TriggerParameterType = Field(..., description="The type of the parameter")
auto_generate: Optional[ParameterAutoGenerate] = Field(
default=None, description="The auto generate of the parameter"
)
template: Optional[ParameterTemplate] = Field(default=None, description="The template of the parameter")
scope: Optional[str] = None
required: Optional[bool] = False
default: Union[int, float, str, None] = None
min: Union[float, int, None] = None
max: Union[float, int, None] = None
precision: Optional[int] = None
options: Optional[list[TriggerParameterOption]] = None
description: Optional[I18nObject] = None
class TriggerProviderIdentity(BaseModel):
"""
The identity of the trigger provider
"""
author: str = Field(..., description="The author of the trigger provider")
name: str = Field(..., description="The name of the trigger provider")
label: I18nObject = Field(..., description="The label of the trigger provider")
description: I18nObject = Field(..., description="The description of the trigger provider")
icon: Optional[str] = Field(default=None, description="The icon of the trigger provider")
tags: list[str] = Field(default_factory=list, description="The tags of the trigger provider")
class TriggerIdentity(BaseModel):
"""
The identity of the trigger
"""
author: str = Field(..., description="The author of the trigger")
name: str = Field(..., description="The name of the trigger")
label: I18nObject = Field(..., description="The label of the trigger")
class TriggerDescription(BaseModel):
"""
The description of the trigger
"""
human: I18nObject = Field(..., description="Human readable description")
llm: I18nObject = Field(..., description="LLM readable description")
class TriggerConfigurationExtraPython(BaseModel):
"""Python configuration for trigger"""
source: str = Field(..., description="The source file path for the trigger implementation")
class TriggerConfigurationExtra(BaseModel):
"""
The extra configuration for trigger
"""
class TriggerEntity(BaseModel):
"""
The configuration of a trigger
"""
python: TriggerConfigurationExtraPython
identity: TriggerIdentity = Field(..., description="The identity of the trigger")
parameters: list[TriggerParameter] = Field(default=[], description="The parameters of the trigger")
description: TriggerDescription = Field(..., description="The description of the trigger")
extra: TriggerConfigurationExtra = Field(..., description="The extra configuration of the trigger")
output_schema: Optional[Mapping[str, Any]] = Field(
default=None, description="The output schema that this trigger produces"
)
class TriggerProviderConfigurationExtraPython(BaseModel):
"""Python configuration for trigger provider"""
source: str = Field(..., description="The source file path for the trigger provider implementation")
class TriggerProviderConfigurationExtra(BaseModel):
"""
The extra configuration for trigger provider
"""
python: TriggerProviderConfigurationExtraPython
class OAuthSchema(BaseModel):
"""OAuth configuration schema"""
authorization_url: str = Field(..., description="OAuth authorization URL")
token_url: str = Field(..., description="OAuth token URL")
client_id: str = Field(..., description="OAuth client ID")
client_secret: str = Field(..., description="OAuth client secret")
redirect_uri: Optional[str] = Field(default=None, description="OAuth redirect URI")
scope: Optional[str] = Field(default=None, description="OAuth scope")
class ProviderConfig(BaseModel):
"""Provider configuration item"""
name: str = Field(..., description="Configuration field name")
type: str = Field(..., description="Configuration field type")
required: bool = Field(default=False, description="Whether this field is required")
default: Any = Field(default=None, description="Default value")
label: Optional[I18nObject] = Field(default=None, description="Field label")
description: Optional[I18nObject] = Field(default=None, description="Field description")
options: Optional[list[dict[str, Any]]] = Field(default=None, description="Options for select type")
class TriggerProviderEntity(BaseModel):
"""
The configuration of a trigger provider
"""
identity: TriggerProviderIdentity = Field(..., description="The identity of the trigger provider")
credentials_schema: list[ProviderConfig] = Field(
default_factory=list,
description="The credentials schema of the trigger provider",
)
oauth_schema: Optional[OAuthSchema] = Field(
default=None,
description="The OAuth schema of the trigger provider if OAuth is supported",
)
subscription_schema: list[ProviderConfig] = Field(
default_factory=list,
description="The subscription schema for trigger(webhook, polling, etc.) subscription parameters",
)
triggers: list[TriggerEntity] = Field(default=[], description="The triggers of the trigger provider")
extra: TriggerProviderConfigurationExtra = Field(..., description="The extra configuration of the trigger provider")
class Subscription(BaseModel):
"""
Result of a successful trigger subscription operation.
Contains all information needed to manage the subscription lifecycle.
"""
expire_at: int = Field(
..., description="The timestamp when the subscription will expire, this for refresh the subscription"
)
metadata: dict[str, Any] = Field(
..., description="Metadata about the subscription in the external service, defined in subscription_schema"
)
class Unsubscription(BaseModel):
"""
Result of a trigger unsubscription operation.
Provides detailed information about the unsubscription attempt,
including success status and error details if failed.
"""
success: bool = Field(..., description="Whether the unsubscription was successful")
message: Optional[str] = Field(
None,
description="Human-readable message about the operation result. "
"Success message for successful operations, "
"detailed error information for failures.",
)
# Export all entities
__all__ = [
"OAuthSchema",
"ParameterAutoGenerate",
"ParameterTemplate",
"ProviderConfig",
"Subscription",
"TriggerConfigurationExtra",
"TriggerConfigurationExtraPython",
"TriggerDescription",
"TriggerEntity",
"TriggerEntity",
"TriggerIdentity",
"TriggerParameter",
"TriggerParameterOption",
"TriggerParameterType",
"TriggerProviderConfigurationExtra",
"TriggerProviderConfigurationExtraPython",
"TriggerProviderEntity",
"TriggerProviderIdentity",
"Unsubscription",
]

View File

@ -0,0 +1,199 @@
"""
Trigger Provider Controller for managing trigger providers
"""
import logging
import time
from typing import Optional
from core.plugin.entities.plugin_daemon import CredentialType
from core.trigger.entities import (
ProviderConfig,
Subscription,
TriggerEntity,
TriggerProviderEntity,
TriggerProviderIdentity,
Unsubscription,
)
logger = logging.getLogger(__name__)
class TriggerProviderController:
"""
Controller for plugin trigger providers
"""
def __init__(
self,
entity: TriggerProviderEntity,
plugin_id: str,
plugin_unique_identifier: str,
tenant_id: str,
):
"""
Initialize plugin trigger provider controller
:param entity: Trigger provider entity
:param plugin_id: Plugin ID
:param plugin_unique_identifier: Plugin unique identifier
:param tenant_id: Tenant ID
"""
self.entity = entity
self.tenant_id = tenant_id
self.plugin_id = plugin_id
self.plugin_unique_identifier = plugin_unique_identifier
@property
def identity(self) -> TriggerProviderIdentity:
"""Get provider identity"""
return self.entity.identity
def get_triggers(self) -> list[TriggerEntity]:
"""
Get all triggers for this provider
:return: List of trigger entities
"""
return self.entity.triggers
def get_trigger(self, trigger_name: str) -> Optional[TriggerEntity]:
"""
Get a specific trigger by name
:param trigger_name: Trigger name
:return: Trigger entity or None
"""
for trigger in self.entity.triggers:
if trigger.identity.name == trigger_name:
return trigger
return None
def get_credentials_schema(self) -> list[ProviderConfig]:
"""
Get credentials schema for this provider
:return: List of provider config schemas
"""
return self.entity.credentials_schema
def get_subscription_schema(self) -> list[ProviderConfig]:
"""
Get subscription schema for this provider
:return: List of subscription config schemas
"""
return self.entity.subscription_schema
def validate_credentials(self, credentials: dict) -> None:
"""
Validate credentials against schema
:param credentials: Credentials to validate
:raises ValueError: If credentials are invalid
"""
for config in self.entity.credentials_schema:
if config.required and config.name not in credentials:
raise ValueError(f"Missing required credential field: {config.name}")
def get_supported_credential_types(self) -> list[CredentialType]:
"""
Get supported credential types for this provider.
:return: List of supported credential types
"""
types = []
if self.entity.oauth_schema:
types.append(CredentialType.OAUTH2)
if self.entity.credentials_schema:
types.append(CredentialType.API_KEY)
return types
def get_credentials_schema_by_type(self, credential_type: str) -> list[ProviderConfig]:
"""
Get credentials schema by credential type
:param credential_type: The type of credential (oauth or api_key)
:return: List of provider config schemas
"""
if credential_type == CredentialType.OAUTH2.value:
return self.entity.oauth_schema.credentials_schema.copy() if self.entity.oauth_schema else []
if credential_type == CredentialType.API_KEY.value:
return self.entity.credentials_schema.copy() if self.entity.credentials_schema else []
raise ValueError(f"Invalid credential type: {credential_type}")
def get_oauth_client_schema(self) -> list[ProviderConfig]:
"""
Get OAuth client schema for this provider
:return: List of OAuth client config schemas
"""
return self.entity.oauth_schema.client_schema.copy() if self.entity.oauth_schema else []
@property
def need_credentials(self) -> bool:
"""Check if this provider needs credentials"""
return len(self.get_supported_credential_types()) > 0
def execute_trigger(self, trigger_name: str, parameters: dict, credentials: dict) -> dict:
"""
Execute a trigger through plugin runtime
:param trigger_name: Trigger name
:param parameters: Trigger parameters
:param credentials: Provider credentials
:return: Execution result
"""
logger.info("Executing trigger %s for plugin %s", trigger_name, self.plugin_id)
return {
"success": True,
"trigger": trigger_name,
"plugin": self.plugin_id,
"result": "Trigger executed successfully",
}
def subscribe_trigger(self, trigger_name: str, subscription_params: dict, credentials: dict) -> Subscription:
"""
Subscribe to a trigger through plugin runtime
:param trigger_name: Trigger name
:param subscription_params: Subscription parameters
:param credentials: Provider credentials
:return: Subscription result
"""
logger.info("Subscribing to trigger %s for plugin %s", trigger_name, self.plugin_id)
return Subscription(
expire_at=int(time.time()) + 86400, # 24 hours from now
metadata={
"subscription_id": f"{self.plugin_id}_{trigger_name}_{time.time()}",
"webhook_url": f"/triggers/webhook/{self.plugin_id}/{trigger_name}",
**subscription_params,
},
)
def unsubscribe_trigger(self, trigger_name: str, subscription_metadata: dict, credentials: dict) -> Unsubscription:
"""
Unsubscribe from a trigger through plugin runtime
:param trigger_name: Trigger name
:param subscription_metadata: Subscription metadata
:param credentials: Provider credentials
:return: Unsubscription result
"""
logger.info("Unsubscribing from trigger %s for plugin %s", trigger_name, self.plugin_id)
return Unsubscription(success=True, message=f"Successfully unsubscribed from trigger {trigger_name}")
def handle_webhook(self, webhook_path: str, request_data: dict, credentials: dict) -> dict:
"""
Handle incoming webhook through plugin runtime
:param webhook_path: Webhook path
:param request_data: Request data
:param credentials: Provider credentials
:return: Webhook handling result
"""
logger.info("Handling webhook for path %s for plugin %s", webhook_path, self.plugin_id)
return {"success": True, "path": webhook_path, "plugin": self.plugin_id, "data_received": request_data}
__all__ = ["TriggerProviderController"]

View File

@ -0,0 +1,360 @@
"""
Trigger Manager for loading and managing trigger providers and triggers
"""
import logging
from typing import Optional
from core.trigger.entities import (
ProviderConfig,
TriggerEntity,
)
from core.trigger.plugin_trigger import PluginTriggerController
from core.trigger.provider import PluginTriggerProviderController
logger = logging.getLogger(__name__)
class TriggerManager:
"""
Manager for trigger providers and triggers
"""
@classmethod
def list_plugin_trigger_providers(cls, tenant_id: str) -> list[PluginTriggerProviderController]:
"""
List all plugin trigger providers for a tenant
:param tenant_id: Tenant ID
:return: List of trigger provider controllers
"""
manager = PluginTriggerController()
provider_entities = manager.fetch_trigger_providers(tenant_id)
controllers = []
for provider in provider_entities:
try:
controller = PluginTriggerProviderController(
entity=provider.declaration,
plugin_id=provider.plugin_id,
plugin_unique_identifier=provider.plugin_unique_identifier,
tenant_id=tenant_id,
)
controllers.append(controller)
except Exception as e:
logger.exception("Failed to load trigger provider {provider.plugin_id}")
continue
return controllers
@classmethod
def get_plugin_trigger_provider(
cls, tenant_id: str, plugin_id: str, provider_name: str
) -> Optional[PluginTriggerProviderController]:
"""
Get a specific plugin trigger provider
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:return: Trigger provider controller or None
"""
manager = PluginTriggerManager()
provider = manager.fetch_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return None
try:
return PluginTriggerProviderController(
entity=provider.declaration,
plugin_id=provider.plugin_id,
plugin_unique_identifier=provider.plugin_unique_identifier,
tenant_id=tenant_id,
)
except Exception as e:
logger.exception("Failed to load trigger provider")
return None
@classmethod
def list_all_trigger_providers(cls, tenant_id: str) -> list[PluginTriggerProviderController]:
"""
List all trigger providers (plugin and builtin)
:param tenant_id: Tenant ID
:return: List of all trigger provider controllers
"""
providers = []
# Get plugin providers
plugin_providers = cls.list_plugin_trigger_providers(tenant_id)
providers.extend(plugin_providers)
# TODO: Add builtin providers when implemented
# builtin_providers = cls.list_builtin_trigger_providers(tenant_id)
# providers.extend(builtin_providers)
return providers
@classmethod
def list_triggers_by_provider(cls, tenant_id: str, plugin_id: str, provider_name: str) -> list[TriggerEntity]:
"""
List all triggers for a specific provider
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:return: List of trigger entities
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return []
return provider.get_triggers()
@classmethod
def get_trigger(
cls, tenant_id: str, plugin_id: str, provider_name: str, trigger_name: str
) -> Optional[TriggerEntity]:
"""
Get a specific trigger
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:param trigger_name: Trigger name
:return: Trigger entity or None
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return None
return provider.get_trigger(trigger_name)
@classmethod
def validate_trigger_credentials(
cls, tenant_id: str, plugin_id: str, provider_name: str, credentials: dict
) -> tuple[bool, str]:
"""
Validate trigger provider credentials
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:param credentials: Credentials to validate
:return: Tuple of (is_valid, error_message)
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return False, "Provider not found"
try:
provider.validate_credentials(credentials)
return True, ""
except Exception as e:
return False, str(e)
@classmethod
def execute_trigger(
cls, tenant_id: str, plugin_id: str, provider_name: str, trigger_name: str, parameters: dict, credentials: dict
) -> dict:
"""
Execute a trigger
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:param trigger_name: Trigger name
:param parameters: Trigger parameters
:param credentials: Provider credentials
:return: Trigger execution result
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
raise ValueError(f"Provider {plugin_id}/{provider_name} not found")
trigger = provider.get_trigger(trigger_name)
if not trigger:
raise ValueError(f"Trigger {trigger_name} not found in provider {provider_name}")
return provider.execute_trigger(trigger_name, parameters, credentials)
@classmethod
def subscribe_trigger(
cls,
tenant_id: str,
plugin_id: str,
provider_name: str,
trigger_name: str,
subscription_params: dict,
credentials: dict,
) -> dict:
"""
Subscribe to a trigger (e.g., register webhook)
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:param trigger_name: Trigger name
:param subscription_params: Subscription parameters
:param credentials: Provider credentials
:return: Subscription result
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
raise ValueError(f"Provider {plugin_id}/{provider_name} not found")
return provider.subscribe_trigger(trigger_name, subscription_params, credentials)
@classmethod
def unsubscribe_trigger(
cls,
tenant_id: str,
plugin_id: str,
provider_name: str,
trigger_name: str,
subscription_metadata: dict,
credentials: dict,
) -> dict:
"""
Unsubscribe from a trigger
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:param trigger_name: Trigger name
:param subscription_metadata: Subscription metadata from subscribe operation
:param credentials: Provider credentials
:return: Unsubscription result
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
raise ValueError(f"Provider {plugin_id}/{provider_name} not found")
return provider.unsubscribe_trigger(trigger_name, subscription_metadata, credentials)
@classmethod
def handle_webhook(
cls,
tenant_id: str,
plugin_id: str,
provider_name: str,
webhook_path: str,
request_data: dict,
credentials: dict,
) -> dict:
"""
Handle incoming webhook for a trigger
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:param webhook_path: Webhook path
:param request_data: Webhook request data
:param credentials: Provider credentials
:return: Webhook handling result
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
raise ValueError(f"Provider {plugin_id}/{provider_name} not found")
return provider.handle_webhook(webhook_path, request_data, credentials)
@classmethod
def get_provider_credentials_schema(
cls, tenant_id: str, plugin_id: str, provider_name: str
) -> list[ProviderConfig]:
"""
Get provider credentials schema
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:return: List of provider config schemas
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return []
return provider.get_credentials_schema()
@classmethod
def get_provider_subscription_schema(
cls, tenant_id: str, plugin_id: str, provider_name: str
) -> list[ProviderConfig]:
"""
Get provider subscription schema
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:return: List of subscription config schemas
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return []
return provider.get_subscription_schema()
@classmethod
def get_provider_info(cls, tenant_id: str, plugin_id: str, provider_name: str) -> Optional[dict]:
"""
Get provider information
:param tenant_id: Tenant ID
:param plugin_id: Plugin ID
:param provider_name: Provider name
:return: Provider info dict or None
"""
provider = cls.get_plugin_trigger_provider(tenant_id, plugin_id, provider_name)
if not provider:
return None
return {
"plugin_id": plugin_id,
"provider_name": provider_name,
"identity": provider.entity.identity.model_dump() if provider.entity.identity else {},
"credentials_schema": [c.model_dump() for c in provider.entity.credentials_schema],
"subscription_schema": [s.model_dump() for s in provider.entity.subscription_schema],
"oauth_enabled": provider.entity.oauth_schema is not None,
"trigger_count": len(provider.entity.triggers),
"triggers": [t.identity.model_dump() for t in provider.entity.triggers],
}
@classmethod
def list_providers_for_workflow(cls, tenant_id: str) -> list[dict]:
"""
List trigger providers suitable for workflow usage
:param tenant_id: Tenant ID
:return: List of provider info dicts
"""
providers = cls.list_all_trigger_providers(tenant_id)
result = []
for provider in providers:
info = {
"plugin_id": provider.plugin_id,
"provider_name": provider.entity.identity.name,
"label": provider.entity.identity.label.model_dump(),
"description": provider.entity.identity.description.model_dump(),
"icon": provider.entity.identity.icon,
"trigger_count": len(provider.entity.triggers),
}
result.append(info)
return result
# Export
__all__ = ["TriggerManager"]