Files
Harry d6fa7bfb7e chore: bump dify_plugin version to 0.6.0b7 and update trigger event handling
- Updated the version in pyproject.toml from 0.6.0b6 to 0.6.0b7.
- Renamed trigger-related methods and classes for clarity, changing invoke_trigger to invoke_trigger_event and updating associated request and response classes.
- Adjusted event handling in the GithubTrigger class to improve event dispatching.

These changes ensure the plugin is up-to-date and enhance the clarity and functionality of event handling.
2025-10-10 19:31:28 +08:00

543 lines
21 KiB
Python

import base64
import logging
import uuid
from collections.abc import Generator
from typing import Any
from pydantic import RootModel
from yarl import URL
from dify_plugin.config.config import DifyPluginEnv, InstallMethod
from dify_plugin.config.logger_format import plugin_logger_handler
from dify_plugin.core.entities.message import InitializeMessage
from dify_plugin.core.entities.plugin.request import (
AgentActions,
DatasourceActions,
DynamicParameterActions,
EndpointActions,
ModelActions,
OAuthActions,
PluginInvokeType,
ToolActions,
TriggerActions,
)
from dify_plugin.core.plugin_executor import PluginExecutor
from dify_plugin.core.plugin_registration import PluginRegistration
from dify_plugin.core.runtime import Session
from dify_plugin.core.server.__base.request_reader import RequestReader
from dify_plugin.core.server.__base.response_writer import ResponseWriter
from dify_plugin.core.server.io_server import IOServer
from dify_plugin.core.server.router import Router
from dify_plugin.core.server.serverless.request_reader import ServerlessRequestReader
from dify_plugin.core.server.stdio.request_reader import StdioRequestReader
from dify_plugin.core.server.stdio.response_writer import StdioResponseWriter
from dify_plugin.core.server.tcp.request_reader import TCPReaderWriter
from dify_plugin.entities.tool import ToolInvokeMessage
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(plugin_logger_handler)
class Plugin(IOServer, Router):
def __init__(self, config: DifyPluginEnv) -> None:
"""
Initialize plugin
"""
# load plugin configuration
self.registration = PluginRegistration(config)
if InstallMethod.Local == config.INSTALL_METHOD:
request_reader, response_writer = self._launch_local_stream(config)
elif InstallMethod.Remote == config.INSTALL_METHOD:
request_reader, response_writer = self._launch_remote_stream(config)
elif InstallMethod.Serverless == config.INSTALL_METHOD:
request_reader, response_writer = self._launch_serverless_stream(config)
else:
raise ValueError("Invalid install method")
# set default response writer
self.default_response_writer = response_writer
# initialize plugin executor
self.plugin_executer = PluginExecutor(config, self.registration)
IOServer.__init__(self, config, request_reader, response_writer)
Router.__init__(self, request_reader, response_writer)
# register io routes
self._register_request_routes()
def _launch_local_stream(self, config: DifyPluginEnv) -> tuple[RequestReader, ResponseWriter | None]:
"""
Launch local stream
"""
reader = StdioRequestReader()
writer = StdioResponseWriter()
writer.write(self.registration.configuration.model_dump_json() + "\n\n")
self._log_configuration()
return reader, writer
def _launch_remote_stream(self, config: DifyPluginEnv) -> tuple[RequestReader, ResponseWriter | None]:
"""
Launch remote stream
"""
if not config.REMOTE_INSTALL_KEY:
raise ValueError("Missing remote install key")
install_host, install_port = self._get_remote_install_host_and_port(config)
logging.debug(f"Remote installing to {install_host}:{install_port}")
tcp_stream = TCPReaderWriter(
install_host,
install_port,
config.REMOTE_INSTALL_KEY,
on_connected=lambda: self._initialize_tcp_stream(tcp_stream),
)
tcp_stream.launch()
return tcp_stream, tcp_stream
def _initialize_tcp_stream(
self,
tcp_stream: TCPReaderWriter,
):
class List(RootModel):
root: list[Any]
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.MANIFEST_DECLARATION,
data=self.registration.configuration.model_dump(),
).model_dump_json()
+ "\n\n"
)
if self.registration.tools_configuration:
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.TOOL_DECLARATION,
data=List(root=self.registration.tools_configuration).model_dump(),
).model_dump_json()
+ "\n\n"
)
if self.registration.models_configuration:
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.MODEL_DECLARATION,
data=List(root=self.registration.models_configuration).model_dump(),
).model_dump_json()
+ "\n\n"
)
if self.registration.endpoints_configuration:
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.ENDPOINT_DECLARATION,
data=List(root=self.registration.endpoints_configuration).model_dump(),
).model_dump_json()
+ "\n\n"
)
if self.registration.agent_strategies_configuration:
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.AGENT_STRATEGY_DECLARATION,
data=List(root=self.registration.agent_strategies_configuration).model_dump(),
).model_dump_json()
+ "\n\n"
)
if self.registration.datasource_configuration:
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.DATASOURCE_DECLARATION,
data=List(root=self.registration.datasource_configuration).model_dump(),
).model_dump_json()
+ "\n\n"
)
if self.registration.triggers_configuration:
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.TRIGGER_DECLARATION,
data=List(root=self.registration.triggers_configuration).model_dump(),
).model_dump_json()
+ "\n\n"
)
for file in self.registration.files:
# divide the file into chunks
chunks = [file.data[i : i + 8192] for i in range(0, len(file.data), 8192)]
for sequence, chunk in enumerate(chunks):
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.ASSET_CHUNK,
data=InitializeMessage.AssetChunk(
filename=file.filename,
data=base64.b64encode(chunk).decode(),
end=sequence == len(chunks) - 1,
).model_dump(),
).model_dump_json()
+ "\n\n"
)
tcp_stream.write(
InitializeMessage(
type=InitializeMessage.Type.END,
data={},
).model_dump_json()
+ "\n\n"
)
self._log_configuration()
def _launch_serverless_stream(self, config: DifyPluginEnv) -> tuple[RequestReader, ResponseWriter | None]:
"""
Launch Serverless stream
"""
serverless = ServerlessRequestReader(
config.SERVERLESS_HOST,
config.SERVERLESS_PORT,
config.SERVERLESS_WORKER_CLASS,
config.SERVERLESS_WORKERS,
config.SERVERLESS_THREADS,
config.MAX_REQUEST_TIMEOUT,
)
serverless.launch()
return serverless, None
def _log_configuration(self):
"""
Log plugin configuration
"""
for tool in self.registration.tools_configuration:
logger.info(f"Installed tool: {tool.identity.name}")
for model in self.registration.models_configuration:
logger.info(f"Installed model: {model.provider}")
for endpoint in self.registration.endpoints_configuration:
logger.info(f"Installed endpoint: {[e.path for e in endpoint.endpoints]}")
for agent in self.registration.agent_strategies_configuration:
logger.info(f"Installed agent: {agent.identity.name}")
for trigger_provider in self.registration.triggers_configuration:
logger.info(f"Installed trigger provider: {trigger_provider.identity.name}")
def _register_request_routes(self):
"""
Register routes
"""
self.register_route(
self.plugin_executer.invoke_tool,
lambda data: data.get("type") == PluginInvokeType.Tool.value
and data.get("action") == ToolActions.InvokeTool.value,
)
self.register_route(
self.plugin_executer.validate_tool_provider_credentials,
lambda data: data.get("type") == PluginInvokeType.Tool.value
and data.get("action") == ToolActions.ValidateCredentials.value,
)
self.register_route(
self.plugin_executer.invoke_agent_strategy,
lambda data: data.get("type") == PluginInvokeType.Agent.value
and data.get("action") == AgentActions.InvokeAgentStrategy.value,
)
self.register_route(
self.plugin_executer.invoke_llm,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.InvokeLLM.value,
)
self.register_route(
self.plugin_executer.get_llm_num_tokens,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.GetLLMNumTokens.value,
)
self.register_route(
self.plugin_executer.invoke_text_embedding,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.InvokeTextEmbedding.value,
)
self.register_route(
self.plugin_executer.get_text_embedding_num_tokens,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.GetTextEmbeddingNumTokens.value,
)
self.register_route(
self.plugin_executer.invoke_rerank,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.InvokeRerank.value,
)
self.register_route(
self.plugin_executer.invoke_tts,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.InvokeTTS.value,
)
self.register_route(
self.plugin_executer.get_tts_model_voices,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.GetTTSVoices.value,
)
self.register_route(
self.plugin_executer.invoke_speech_to_text,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.InvokeSpeech2Text.value,
)
self.register_route(
self.plugin_executer.invoke_moderation,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.InvokeModeration.value,
)
self.register_route(
self.plugin_executer.validate_model_provider_credentials,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.ValidateProviderCredentials.value,
)
self.register_route(
self.plugin_executer.validate_model_credentials,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.ValidateModelCredentials.value,
)
self.register_route(
self.plugin_executer.invoke_endpoint,
lambda data: data.get("type") == PluginInvokeType.Endpoint.value
and data.get("action") == EndpointActions.InvokeEndpoint.value,
)
self.register_route(
self.plugin_executer.get_ai_model_schemas,
lambda data: data.get("type") == PluginInvokeType.Model.value
and data.get("action") == ModelActions.GetAIModelSchemas.value,
)
self.register_route(
self.plugin_executer.validate_datasource_credentials,
lambda data: data.get("type") == PluginInvokeType.Datasource.value
and data.get("action") == DatasourceActions.ValidateCredentials.value,
)
self.register_route(
self.plugin_executer.datasource_crawl_website,
lambda data: data.get("type") == PluginInvokeType.Datasource.value
and data.get("action") == DatasourceActions.InvokeWebsiteDatasourceGetCrawl.value,
)
self.register_route(
self.plugin_executer.datasource_get_page_content,
lambda data: data.get("type") == PluginInvokeType.Datasource.value
and data.get("action") == DatasourceActions.InvokeOnlineDocumentDatasourceGetPageContent.value,
)
self.register_route(
self.plugin_executer.datasource_get_pages,
lambda data: data.get("type") == PluginInvokeType.Datasource.value
and data.get("action") == DatasourceActions.InvokeOnlineDocumentDatasourceGetPages.value,
)
self.register_route(
self.plugin_executer.get_oauth_authorization_url,
lambda data: data.get("type") == PluginInvokeType.OAuth.value
and data.get("action") == OAuthActions.GetAuthorizationUrl.value,
)
self.register_route(
self.plugin_executer.get_oauth_credentials,
lambda data: data.get("type") == PluginInvokeType.OAuth.value
and data.get("action") == OAuthActions.GetCredentials.value,
)
self.register_route(
self.plugin_executer.refresh_oauth_credentials,
lambda data: data.get("type") == PluginInvokeType.OAuth.value
and data.get("action") == OAuthActions.RefreshCredentials.value,
)
self.register_route(
self.plugin_executer.datasource_online_drive_browse_files,
lambda data: data.get("type") == PluginInvokeType.Datasource.value
and data.get("action") == DatasourceActions.InvokeOnlineDriveBrowseFiles.value,
)
self.register_route(
self.plugin_executer.datasource_online_drive_download_file,
lambda data: data.get("type") == PluginInvokeType.Datasource.value
and data.get("action") == DatasourceActions.InvokeOnlineDriveDownloadFile.value,
)
self.register_route(
self.plugin_executer.fetch_parameter_options,
lambda data: data.get("type") == PluginInvokeType.DynamicParameter.value
and data.get("action") == DynamicParameterActions.FetchParameterOptions.value,
)
# Trigger routes
self.register_route(
self.plugin_executer.invoke_trigger_event,
lambda data: data.get("type") == PluginInvokeType.Trigger.value
and data.get("action") == TriggerActions.InvokeTriggerEvent.value,
)
self.register_route(
self.plugin_executer.validate_trigger_provider_credentials,
lambda data: data.get("type") == PluginInvokeType.Trigger.value
and data.get("action") == TriggerActions.ValidateProviderCredentials.value,
)
self.register_route(
self.plugin_executer.dispatch_trigger_event,
lambda data: data.get("type") == PluginInvokeType.Trigger.value
and data.get("action") == TriggerActions.DispatchTriggerEvent.value,
)
self.register_route(
self.plugin_executer.subscribe_trigger,
lambda data: data.get("type") == PluginInvokeType.Trigger.value
and data.get("action") == TriggerActions.SubscribeTrigger.value,
)
self.register_route(
self.plugin_executer.unsubscribe_trigger,
lambda data: data.get("type") == PluginInvokeType.Trigger.value
and data.get("action") == TriggerActions.UnsubscribeTrigger.value,
)
self.register_route(
self.plugin_executer.refresh_trigger,
lambda data: data.get("type") == PluginInvokeType.Trigger.value
and data.get("action") == TriggerActions.RefreshTrigger.value,
)
def _execute_request(
self,
session_id: str,
data: dict,
reader: RequestReader,
writer: ResponseWriter,
conversation_id: str | None = None,
message_id: str | None = None,
app_id: str | None = None,
endpoint_id: str | None = None,
context: dict | None = None,
):
"""
accept requests and execute
:param session_id: session id, unique for each request
:param data: request data
"""
session = Session(
session_id=session_id,
executor=self.executer,
reader=reader,
writer=writer,
install_method=self.config.INSTALL_METHOD,
dify_plugin_daemon_url=self.config.DIFY_PLUGIN_DAEMON_URL,
conversation_id=conversation_id,
message_id=message_id,
app_id=app_id,
endpoint_id=endpoint_id,
context=context,
max_invocation_timeout=self.config.MAX_INVOCATION_TIMEOUT,
)
response = self.dispatch(session, data)
if response:
if isinstance(response, Generator):
for message in response:
if isinstance(message, ToolInvokeMessage) and isinstance(
message.message, ToolInvokeMessage.BlobMessage
):
# convert blob to file chunks
id_ = uuid.uuid4().hex
blob = message.message.blob
message.message.blob = id_.encode("utf-8")
# split the blob into chunks
chunks = [blob[i : i + 8192] for i in range(0, len(blob), 8192)]
for sequence, chunk in enumerate(chunks):
writer.session_message(
session_id=session_id,
data=writer.stream_object(
data=ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id=id_,
sequence=sequence,
total_length=len(blob),
blob=chunk,
end=False,
),
meta=message.meta,
),
),
)
# end the file stream
writer.session_message(
session_id=session_id,
data=writer.stream_object(
data=ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id=id_,
sequence=len(chunks),
total_length=len(blob),
blob=b"",
end=True,
),
meta=message.meta,
)
),
)
else:
writer.session_message(
session_id=session_id,
data=writer.stream_object(data=message),
)
else:
writer.session_message(
session_id=session_id,
data=writer.stream_object(data=response),
)
@staticmethod
def _get_remote_install_host_and_port(config: DifyPluginEnv) -> tuple[str, int]:
"""
Get host and port for remote installation
:param config: Dify plugin env config
:return: host and port
"""
install_url = config.REMOTE_INSTALL_URL
if install_url is not None:
if ":" in install_url:
url = URL(install_url)
if url.host and url.port:
# for the url with protocol prefix
host = url.host
port = url.port
else:
# for "host:port" format
split = install_url.split(":")
host = split[0]
port = int(split[1])
else:
raise ValueError(
f'Invalid remote install URL {install_url}, which should be in the format of "host:port"'
)
else:
host = config.REMOTE_INSTALL_HOST
port = config.REMOTE_INSTALL_PORT
return host, port