支持私有云部署
AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


扩展插件 Endpoint:为 Dify 注入 Serverless 新能力

发布日期:2025-03-18 20:14:56 浏览次数: 1591 来源:Dify
推荐语

Dify插件Endpoint:开启Serverless新纪元,自定义API逻辑,打造个性化WebApp。

核心内容:
1. Endpoint插件:Dify v1.0的全新扩展类型,支持自定义API逻辑
2. Serverless特性:Endpoint实现HTTP服务器功能,支持反向调用
3. WebApp模板:通过Endpoint实现风格化定制,打造多样化Chatbot

杨芳贤
53A创始人/腾讯云(TVP)最具价值专家

关于 Endpoint

Endpoint 是 dify 在 v1.0 插件机制中引入的一种全新扩展类型,它为 Dify 新增了一个 API 入口。插件可以通过代码自定义 API 的逻辑。从开发者的角度来看,这相当于支持在 Dify 内运行一个 HTTP 服务器,且服务器的实现由开发者自己决定。我们可以通过下面这张图更好地理解 Endpoint:
Endpoint 的具体逻辑实现在 Plugin 中。当用户启动 Endpoint 时,Dify 会为用户生成一串随机的 URL,格式如 `https://abcdefg.dify.ai`。当 Dify 接收到该 URL 请求时,原始的 HTTP 报文会被转发至插件。此时插件的行为类似于 Serverless,接收并处理请求。
然而,这只是基础功能。为了让插件能够调用 Dify 内的 App,我们引入了反向调用功能。完成这整套协议后,IM 类需求得到了一定程度的闭环。可是,Endpoint 的潜力远不止于此。本文将深入探讨 Endpoint 的能力边界,让我们进一步了解它的实际应用。

审视能力本质

在最初设计 Endpoint 时,它是一个用于处理 Webhook 的模块,旨在通过插件逻辑将复杂且难以泛化的低代码/无代码工作流抽象为可复用的代码实现。因此,我们还引入了诸如反向调用等功能。但随着使用的深入,我们发现 Endpoint 实际上具有更广泛的用途。它本质上就是一个 Serverless HTTP 服务器,虽然不支持长连接协议如 WebSocket,但它可以实现大多数 HTTP 服务器的能力。例如,可以用它来构建套壳 Chatbot。

WebApp 模板

目前 Dify 的 WebApp 还较为基础,风格化定制部分几乎为空白。由于我们难以精雕细琢每一个场景和具体的端侧需求,为何不通过 Endpoint 来实现这些需求呢?假设一个插件中内置了多个 Endpoint,每个 Endpoint 都是风格不同的模板,例如极简风、二次元可爱风、韩式风格或欧美风格。这些不同风格的 Endpoint 背后其实都是同一个 Chatbot,只是皮肤不同。这就形成了一个天然的模板市场。
通过这一规范,我们理论上可以开放 WebApp,让 Dify 用户拥有更多选择,避免被局限在 Dify 生态内。这为用户带来了更好的体验,但这也要求 Dify 生态逐渐趋于繁荣。要实现这一目标,我们还有很长的路要走。

实现

作为示例,我们可以先实现一个简单的版本,包含两个 Endpoint:一个用于展示页面,另一个用于请求 Dify。我们不会在此列出所有开发步骤,具体开发规范请参考帮助文档。
快速开始:https://docs.dify.ai/zh-hans/plugins/quick-start
页面代码如下:
<!DOCTYPE html><html lang="zh">
<body><!-- Header title, displaying ChatBot name --><header><h1>{{ bot_name }}</h1></header>
<div class="chat-container"><div id="chat-log"></div><div class="input-container"><input type="text" id="user-input" placeholder="Press Enter or click Send after typing" /><button id="send-btn">Send</button><!-- Add "Reset Conversation" button --><button id="reset-btn">Reset</button></div></div>
<script>// You can customize the bot nameconst botName = '{{ bot_name }}';
// Get or generate conversation ID from localStorage to support multi-turn dialoguelet conversationId = localStorage.getItem('conversation_id') || '';
// Get page elementsconst chatLog = document.getElementById('chat-log');const userInput = document.getElementById('user-input');const sendBtn = document.getElementById('send-btn');const resetBtn = document.getElementById('reset-btn');
// Bind events to buttons and inputsendBtn.addEventListener('click', sendMessage);userInput.addEventListener('keypress', function (event) {// Send message when Enter key is pressedif (event.key === 'Enter') {sendMessage();}});
// Click reset buttonresetBtn.addEventListener('click', resetConversation);
/** * Send message to backend and handle streaming response */async function sendMessage() {const message = userInput.value.trim();if (!message) return;
// Display user message in chat logappendMessage(message, 'user');userInput.value = '';
// Prepare request bodyconst requestBody = {query: message,conversation_id: conversationId};
try {// Replace with backend streaming API endpointconst response = await fetch('./pink/talk', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(requestBody)});
if (!response.ok) {throw new Error('Network response was not ok');}
// Create a placeholder for displaying ChatBot replylet botMessageContainer = appendMessage('', 'bot');
// Read backend response as streamconst reader = response.body.getReader();const decoder = new TextDecoder('utf-8');
let buffer = '';while (true) {const { value, done } = await reader.read();if (done) break;
buffer += decoder.decode(value, { stream: true });
// Split and process by linesconst lines = buffer.split('\n\n');buffer = lines.pop() || ''; // Keep the last incomplete line
for (const line of lines) {if (!line.trim()) continue;
try {const data = JSON.parse(line);
if (data.answer) {botMessageContainer.textContent += data.answer;}if (data.conversation_id) {conversationId = data.conversation_id;localStorage.setItem('conversation_id', conversationId);}} catch (error) {console.error('Error:', error, line);}}}} catch (error) {console.error('Error:', error);appendMessage('Request failed, please try again later.', 'bot');}}
/** * Insert message into chat log * @param {string} text - Message content * @param {string} sender - 'user' or 'bot' * @returns {HTMLElement} - Returns the current inserted message element for later content updates */function appendMessage(text, sender) {const messageEl = document.createElement('div');messageEl.className = `message ${sender}`;
// If it's bot, display "Bot Name: Message", otherwise display user messageif (sender === 'bot') {messageEl.textContent = botName + ': ' + text;} else {messageEl.textContent = text; // User message}
chatLog.appendChild(messageEl);
// Scroll chat log to bottomchatLog.scrollTop = chatLog.scrollHeight;return messageEl;}
/** * Reset conversation: Clear conversation_id and chat log, initialize example messages */function resetConversation() {// Remove conversation ID from local storagelocalStorage.removeItem('conversation_id');conversationId = '';// Clear chat logchatLog.innerHTML = '';}</script></body>
</html>
我们用一个 Endpoint 来简单地 host 它:

from collections.abc import Mappingimport os
from werkzeug import Request, Response
from dify_plugin import Endpoint
class NekoEndpoint(Endpoint):def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:# read file from girls.html using current python file relative pathwith open(os.path.join(os.path.dirname(__file__), "girls.html"), "r") as f:return Response(f.read().replace("{{ bot_name }}", settings.get("bot_name", "Candy")),status=200,content_type="text/html",)

然后实现一个调用接口的 Endpoint:
from collections.abc import Mappingimport jsonfrom typing import Optional
from werkzeug import Request, Response
from dify_plugin import Endpoint
class GirlsTalk(Endpoint):def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:"""Invokes the endpoint with the given request."""
app: Optional[dict] = settings.get("app")if not app:return Response("App is required", status=400)
data = r.get_json()query = data.get("query")conversation_id = data.get("conversation_id")
if not query:return Response("Query is required", status=400)
def generator():response = self.session.app.chat.invoke(app_id=app.get("app_id"),query=query,inputs={},conversation_id=conversation_id,response_mode="streaming",)
for chunk in response:yield json.dumps(chunk) + "\n\n"
return Response(generator(), status=200, content_type="text/event-stream")
当一切完成后,打开对应的 Endpoint 即可看到这个页面:
这样,我们就给 Dify 套上了不同的皮肤,并在此基础上,经过优化和修改后,形成了一个具备丰富功能的 UI,甚至还加上了 TTS,完成了半个语音 Chatbot:

OpenAI 兼容接口

用户曾提出以下问题:
  1. Dify 支持多家厂商的模型,为什么不把 Dify 作为 API 网关来使用呢?
  2. 为什么 Dify 的 App 无法使用 OpenAI 的兼容格式返回?
我们一直关注这些问题,但 Dify 的 API 是有状态的 API,相比 OpenAI 的无状态 API,它能够实现更多的功能,特别是在上下文管理方面。Dify 通过 `conversation_id` 控制每一次的对话,而 OpenAI 只能每次携带全量上下文。此外,Dify 的 API 提供了更多自定义和扩展的空间。
虽然我们没有立即实现 OpenAI 兼容接口,但在引入 Endpoint 和反向调用后,这些本应耦合在 Dify 本体中的功能变成了插件。通过开发插件调用 Dify 的 LLM,我们可以实现完整的模型转 OpenAI 的需求,也可以通过插件将 Dify API 转为 OpenAI 格式,满足部分用户需求。

实现

以模型统一接口为例,我们可以设置一个 Endpoint 组,如下所示:
settings:- name: api_keytype: secret-inputrequired: truelabel:en_US: API keyzh_Hans: API keypt_BR: API keyplaceholder:en_US: Please input your API keyzh_Hans: 请输入你的 API keypt_BR: Please input your API key- name: llmtype: model-selectorscope: llmrequired: falselabel:en_US: LLMzh_Hans: LLMpt_BR: LLMplaceholder:en_US: Please select a LLMzh_Hans: 请选择一个 LLMpt_BR: Please select a LLM- name: text_embeddingtype: model-selectorscope: text-embeddingrequired: falselabel:en_US: Text Embeddingzh_Hans: 文本嵌入pt_BR: Text Embeddingplaceholder:en_US: Please select a Text Embedding Modelzh_Hans: 请选择一个文本嵌入模型pt_BR: Please select a Text Embedding Modelendpoints:- endpoints/llm.yaml- endpoints/text_embedding.yaml
完成后,我们可以选择想要转换成 OpenAI 接口的模型,如 Claude。
我们可以通过简化后的伪代码来实现,完整代码可以参考:https://github.com/langgenius/dify-official-plugins/blob/main/extensions/oaicompat_dify_model/endpoints/llm.py

class OaicompatDifyModelEndpoint(Endpoint):def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:"""Invokes the endpoint with the given request."""llm: Optional[dict] = settings.get("llm")data = r.get_json(force=True)prompt_messages: list[PromptMessage] = []if not isinstance(data.get("messages"), list) or not data.get("messages"):raise ValueError("Messages is not a list or empty")
for message in data.get("messages", []):# transform messagespasstools: list[PromptMessageTool] = []if data.get("tools"):for tool in data.get("tools", []):tools.append(PromptMessageTool(**tool))
stream: bool = data.get("stream", False)
def generator():if not stream:llm_invoke_response = self.session.model.llm.invoke(model_config=LLMModelConfig(**llm),prompt_messages=prompt_messages,tools=tools,stream=False,)
yield json.dumps({"id": "chatcmpl-" + str(uuid.uuid4()),"object": "chat.completion","created": int(time.time()),"model": llm.get("model"),"choices": [{"index": 0,"message": {"role": "assistant","content": llm_invoke_response.message.content},"finish_reason": "stop"}],"usage": {"prompt_tokens": llm_invoke_response.usage.prompt_tokens,"completion_tokens": llm_invoke_response.usage.completion_tokens,"total_tokens": llm_invoke_response.usage.total_tokens}})else:llm_invoke_response = self.session.model.llm.invoke(model_config=LLMModelConfig(**llm),prompt_messages=prompt_messages,tools=tools,stream=True,)
for chunk in llm_invoke_response:yield json.dumps({"id": "chatcmpl-" + str(uuid.uuid4()),"object": "chat.completion.chunk","created": int(time.time()),"model": llm.get("model"),"choices": [{"index": 0,"delta": {"content": chunk.delta.message.content},"finish_reason": None}]}) + "\n\n"
return Response(generator(), status=200, content_type="event-stream" if stream else "application/json")

当一切完成后,我们可以使用 curl 命令来测试实现是否成功。

异步事件触发

基于事件触发的 Workflow 一直是社区中反馈较多的需求,许多用户的场景涉及异步事件。例如,先发送一个任务,等待任务执行完成后触发信号,继续执行剩余的流程。以前在 Dify 中无法实现这样的需求,但现在通过 Endpoint,我们可以将其拆解为两个 Workflow。第一个 Workflow 发起任务并正常退出,第二个 Workflow 接收 Webhook 信号并继续执行后续流程。
尽管这个过程对用户来说不够直观,但它确实解决了一些问题,例如 AI 批量生成的长文先发布到审核平台,用户审核后点击接受,触发事件返回 Dify 完成后续的发布流程。虽然这在当前技术框架下有些复杂,但未来几个月我们将为 Workflow 提供直接的事件触发能力,进一步优化整体体验。

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询