Feat/assistant app (#2086)

Co-authored-by: chenhe <guchenhe@gmail.com>
Co-authored-by: Pascal M <11357019+perzeuss@users.noreply.github.com>
This commit is contained in:
Yeuoly
2024-01-23 19:58:23 +08:00
committed by GitHub
parent 7bbe12b2bd
commit 86286e1ac8
175 changed files with 11619 additions and 1235 deletions

View File

@@ -0,0 +1,266 @@
# 高级接入Tool
在开始高级接入之前,请确保你已经阅读过[快速接入](./tool_scale_out.md)并对Dify的工具接入流程有了基本的了解。
## 工具接口
我们在`Tool`类中定义了一系列快捷方法,用于帮助开发者快速构较为复杂的工具
### 消息返回
Dify支持`文本` `链接` `图片` `文件BLOB` 等多种消息类型你可以通过以下几个接口返回不同类型的消息给LLM和用户。
注意,在下面的接口中的部分参数将在后面的章节中介绍。
#### 图片URL
只需要传递图片的URL即可Dify会自动下载图片并返回给用户。
```python
def create_image_message(self, image: str, save_as: str = '') -> ToolInvokeMessage:
"""
create an image message
:param image: the url of the image
:return: the image message
"""
```
#### 链接
如果你需要返回一个链接,可以使用以下接口。
```python
def create_link_message(self, link: str, save_as: str = '') -> ToolInvokeMessage:
"""
create a link message
:param link: the url of the link
:return: the link message
"""
```
#### 文本
如果你需要返回一个文本消息,可以使用以下接口。
```python
def create_text_message(self, text: str, save_as: str = '') -> ToolInvokeMessage:
"""
create a text message
:param text: the text of the message
:return: the text message
"""
```
#### 文件BLOB
如果你需要返回文件的原始数据如图片、音频、视频、PPT、Word、Excel等可以使用以下接口。
- `blob` 文件的原始数据bytes类型
- `meta` 文件的元数据,如果你知道该文件的类型,最好传递一个`mime_type`否则Dify将使用`octet/stream`作为默认类型
```python
def create_blob_message(self, blob: bytes, meta: dict = None, save_as: str = '') -> ToolInvokeMessage:
"""
create a blob message
:param blob: the blob
:return: the blob message
"""
```
### 快捷工具
在大模型应用中,我们有两种常见的需求:
- 先将很长的文本进行提前总结然后再将总结内容传递给LLM以防止原文本过长导致LLM无法处理
- 工具获取到的内容是一个链接需要爬取网页信息后再返回给LLM
为了帮助开发者快速实现这两种需求,我们提供了以下两个快捷工具。
#### 文本总结工具
该工具需要传入user_id和需要进行总结的文本返回一个总结后的文本Dify会使用当前工作空间的默认模型对长文本进行总结。
```python
def summary(self, user_id: str, content: str) -> str:
"""
summary the content
:param user_id: the user id
:param content: the content
:return: the summary
"""
```
#### 网页爬取工具
该工具需要传入需要爬取的网页链接和一个user_agent可为空返回一个包含该网页信息的字符串其中`user_agent`是可选参数可以用来识别工具如果不传递Dify将使用默认的`user_agent`
```python
def get_url(self, url: str, user_agent: str = None) -> str:
"""
get url
""" the crawled result
```
### 变量池
我们在`Tool`中引入了一个变量池,用于存储工具运行过程中产生的变量、文件等,这些变量可以在工具运行过程中被其他工具使用。
下面,我们以`DallE3``Vectorizer.AI`为例,介绍如何使用变量池。
- `DallE3`是一个图片生成工具,它可以根据文本生成图片,在这里,我们将让`DallE3`生成一个咖啡厅的Logo
- `Vectorizer.AI`是一个矢量图转换工具,它可以将图片转换为矢量图,使得图片可以无限放大而不失真,在这里,我们将`DallE3`生成的PNG图标转换为矢量图从而可以真正被设计师使用。
#### DallE3
首先我们使用DallE3在创建完图片以后我们将图片保存到变量池中代码如下
```python
from typing import Any, Dict, List, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from base64 import b64decode
from openai import OpenAI
class DallE3Tool(BuiltinTool):
def _invoke(self,
user_id: str,
tool_paramters: Dict[str, Any],
) -> Union[ToolInvokeMessage, List[ToolInvokeMessage]]:
"""
invoke tools
"""
client = OpenAI(
api_key=self.runtime.credentials['openai_api_key'],
)
# prompt
prompt = tool_paramters.get('prompt', '')
if not prompt:
return self.create_text_message('Please input prompt')
# call openapi dalle3
response = client.images.generate(
prompt=prompt, model='dall-e-3',
size='1024x1024', n=1, style='vivid', quality='standard',
response_format='b64_json'
)
result = []
for image in response.data:
# 将所有图片通过save_as参数保存到变量池中变量名为self.VARIABLE_KEY.IMAGE.value如果如果后续有新的图片生成那么将会覆盖之前的图片
result.append(self.create_blob_message(blob=b64decode(image.b64_json),
meta={ 'mime_type': 'image/png' },
save_as=self.VARIABLE_KEY.IMAGE.value))
return result
```
我们可以注意到这里我们使用了`self.VARIABLE_KEY.IMAGE.value`作为图片的变量名,为了便于开发者们的工具能够互相配合,我们定义了这个`KEY`,大家可以自由使用,也可以不使用这个`KEY`传递一个自定义的KEY也是可以的。
#### Vectorizer.AI
接下来我们使用Vectorizer.AI将DallE3生成的PNG图标转换为矢量图我们先来过一遍我们在这里定义的函数代码如下
```python
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParamter
from core.tools.errors import ToolProviderCredentialValidationError
from typing import Any, Dict, List, Union
from httpx import post
from base64 import b64decode
class VectorizerTool(BuiltinTool):
def _invoke(self, user_id: str, tool_paramters: Dict[str, Any]) \
-> Union[ToolInvokeMessage, List[ToolInvokeMessage]]:
"""
工具调用,图片变量名需要从这里传递进来,从而我们就可以从变量池中获取到图片
"""
def get_runtime_parameters(self) -> List[ToolParamter]:
"""
重写工具参数列表我们可以根据当前变量池里的实际情况来动态生成参数列表从而LLM可以根据参数列表来生成表单
"""
def is_tool_avaliable(self) -> bool:
"""
当前工具是否可用如果当前变量池中没有图片那么我们就不需要展示这个工具这里返回False即可
"""
```
接下来我们来实现这三个函数
```python
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParamter
from core.tools.errors import ToolProviderCredentialValidationError
from typing import Any, Dict, List, Union
from httpx import post
from base64 import b64decode
class VectorizerTool(BuiltinTool):
def _invoke(self, user_id: str, tool_paramters: Dict[str, Any]) \
-> Union[ToolInvokeMessage, List[ToolInvokeMessage]]:
"""
invoke tools
"""
api_key_name = self.runtime.credentials.get('api_key_name', None)
api_key_value = self.runtime.credentials.get('api_key_value', None)
if not api_key_name or not api_key_value:
raise ToolProviderCredentialValidationError('Please input api key name and value')
# 获取image_idimage_id的定义可以在get_runtime_parameters中找到
image_id = tool_paramters.get('image_id', '')
if not image_id:
return self.create_text_message('Please input image id')
# 从变量池中获取到之前DallE生成的图片
image_binary = self.get_variable_file(self.VARIABLE_KEY.IMAGE)
if not image_binary:
return self.create_text_message('Image not found, please request user to generate image firstly.')
# 生成矢量图
response = post(
'https://vectorizer.ai/api/v1/vectorize',
files={ 'image': image_binary },
data={ 'mode': 'test' },
auth=(api_key_name, api_key_value),
timeout=30
)
if response.status_code != 200:
raise Exception(response.text)
return [
self.create_text_message('the vectorized svg is saved as an image.'),
self.create_blob_message(blob=response.content,
meta={'mime_type': 'image/svg+xml'})
]
def get_runtime_parameters(self) -> List[ToolParamter]:
"""
override the runtime parameters
"""
# 这里我们重写了工具参数列表定义了image_id并设置了它的选项列表为当前变量池中的所有图片这里的配置与yaml中的配置是一致的
return [
ToolParamter.get_simple_instance(
name='image_id',
llm_description=f'the image id that you want to vectorize, \
and the image id should be specified in \
{[i.name for i in self.list_default_image_variables()]}',
type=ToolParamter.ToolParameterType.SELECT,
required=True,
options=[i.name for i in self.list_default_image_variables()]
)
]
def is_tool_avaliable(self) -> bool:
# 只有当变量池中有图片时LLM才需要使用这个工具
return len(self.list_default_image_variables()) > 0
```
可以注意到的是,我们这里其实并没有使用到`image_id`,我们已经假设了调用这个工具的时候一定有一张图片在默认的变量池中,所以直接使用了`image_binary = self.get_variable_file(self.VARIABLE_KEY.IMAGE)`来获取图片,在模型能力较弱的情况下,我们建议开发者们也这样做,可以有效提升容错率,避免模型传递错误的参数。

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -0,0 +1,212 @@
# 快速接入Tool
这里我们以GoogleSearch为例介绍如何快速接入一个工具。
## 1. 准备工具供应商yaml
### 介绍
这个yaml将包含工具供应商的信息包括供应商名称、图标、作者等详细信息以帮助前端灵活展示。
### 示例
我们需要在 `core/tools/provider/builtin`下创建一个`google`模块(文件夹),并创建`google.yaml`,名称必须与模块名称一致。
后续,我们关于这个工具的所有操作都将在这个模块下进行。
```yaml
identity: # 工具供应商的基本信息
author: Dify # 作者
name: google # 名称,唯一,不允许和其他供应商重名
label: # 标签,用于前端展示
en_US: Google # 英文标签
zh_Hans: Google # 中文标签
description: # 描述,用于前端展示
en_US: Google # 英文描述
zh_Hans: Google # 中文描述
icon: icon.svg # 图标需要放置在当前模块的_assets文件夹下
```
- `identity` 字段是必须的,它包含了工具供应商的基本信息,包括作者、名称、标签、描述、图标等
- 图标需要放置在当前模块的`_assets`文件夹下,可以参考[这里](../../provider/builtin/google/_assets/icon.svg)。
## 2. 准备供应商凭据
Google作为一个第三方工具使用了SerpApi提供的API而SerpApi需要一个API Key才能使用那么就意味着这个工具需要一个凭据才可以使用而像`wikipedia`这样的工具,就不需要填写凭据字段,可以参考[这里](../../provider/builtin/wikipedia/wikipedia.yaml)。
配置好凭据字段后效果如下:
```yaml
identity:
author: Dify
name: google
label:
en_US: Google
zh_Hans: Google
description:
en_US: Google
zh_Hans: Google
icon: icon.svg
credentails_for_provider: # 凭据字段
serpapi_api_key: # 凭据字段名称
type: secret-input # 凭据字段类型
required: true # 是否必填
label: # 凭据字段标签
en_US: SerpApi API key # 英文标签
zh_Hans: SerpApi API key # 中文标签
placeholder: # 凭据字段占位符
en_US: Please input your SerpApi API key # 英文占位符
zh_Hans: 请输入你的 SerpApi API key # 中文占位符
help: # 凭据字段帮助文本
en_US: Get your SerpApi API key from SerpApi # 英文帮助文本
zh_Hans: 从 SerpApi 获取您的 SerpApi API key # 中文帮助文本
url: https://serpapi.com/manage-api-key # 凭据字段帮助链接
```
- `type`:凭据字段类型,目前支持`secret-input``text-input``select` 三种类型,分别对应密码输入框、文本输入框、下拉框,如果为`secret-input`,则会在前端隐藏输入内容,并且后端会对输入内容进行加密。
## 3. 准备工具yaml
一个供应商底下可以有多个工具每个工具都需要一个yaml文件来描述这个文件包含了工具的基本信息、参数、输出等。
仍然以GoogleSearch为例我们需要在`google`模块下创建一个`tools`模块,并创建`tools/google_search.yaml`,内容如下。
```yaml
identity: # 工具的基本信息
name: google_search # 工具名称,唯一,不允许和其他工具重名
author: Dify # 作者
label: # 标签,用于前端展示
en_US: GoogleSearch # 英文标签
zh_Hans: 谷歌搜索 # 中文标签
description: # 描述,用于前端展示
human: # 用于前端展示的介绍,支持多语言
en_US: A tool for performing a Google SERP search and extracting snippets and webpages.Input should be a search query.
zh_Hans: 一个用于执行 Google SERP 搜索并提取片段和网页的工具。输入应该是一个搜索查询。
llm: A tool for performing a Google SERP search and extracting snippets and webpages.Input should be a search query. # 传递给LLM的介绍为了使得LLM更好理解这个工具我们建议在这里写上关于这个工具尽可能详细的信息让LLM能够理解并使用这个工具
parameters: # 参数列表
- name: query # 参数名称
type: string # 参数类型
required: true # 是否必填
label: # 参数标签
en_US: Query string # 英文标签
zh_Hans: 查询语句 # 中文标签
human_description: # 用于前端展示的介绍,支持多语言
en_US: used for searching
zh_Hans: 用于搜索网页内容
llm_description: key words for searching # 传递给LLM的介绍同上为了使得LLM更好理解这个参数我们建议在这里写上关于这个参数尽可能详细的信息让LLM能够理解这个参数
form: llm # 表单类型llm表示这个参数需要由Agent自行推理出来前端将不会展示这个参数
- name: result_type
type: select # 参数类型
required: true
options: # 下拉框选项
- value: text
label:
en_US: text
zh_Hans: 文本
- value: link
label:
en_US: link
zh_Hans: 链接
default: link
label:
en_US: Result type
zh_Hans: 结果类型
human_description:
en_US: used for selecting the result type, text or link
zh_Hans: 用于选择结果类型,使用文本还是链接进行展示
form: form # 表单类型form表示这个参数需要由用户在对话开始前在前端填写
```
- `identity` 字段是必须的,它包含了工具的基本信息,包括名称、作者、标签、描述等
- `parameters` 参数列表
- `name` 参数名称,唯一,不允许和其他参数重名
- `type` 参数类型,目前支持`string``number``boolean``select` 四种类型,分别对应字符串、数字、布尔值、下拉框
- `required` 是否必填
-`llm`模式下如果参数为必填则会要求Agent必须要推理出这个参数
-`form`模式下,如果参数为必填,则会要求用户在对话开始前在前端填写这个参数
- `options` 参数选项
-`llm`模式下Dify会将所有选项传递给LLMLLM可以根据这些选项进行推理
-`form`模式下,`type``select`时,前端会展示这些选项
- `default` 默认值
- `label` 参数标签,用于前端展示
- `human_description` 用于前端展示的介绍,支持多语言
- `llm_description` 传递给LLM的介绍为了使得LLM更好理解这个参数我们建议在这里写上关于这个参数尽可能详细的信息让LLM能够理解这个参数
- `form` 表单类型,目前支持`llm``form`两种类型分别对应Agent自行推理和前端填写
## 4. 准备工具代码
当完成工具的配置以后,我们就可以开始编写工具代码了,主要用于实现工具的逻辑。
`google/tools`模块下创建`google_search.py`,内容如下。
```python
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.entities.tool_entities import ToolInvokeMessage
from typing import Any, Dict, List, Union
class GoogleSearchTool(BuiltinTool):
def _invoke(self,
user_id: str,
tool_paramters: Dict[str, Any],
) -> Union[ToolInvokeMessage, List[ToolInvokeMessage]]:
"""
invoke tools
"""
query = tool_paramters['query']
result_type = tool_paramters['result_type']
api_key = self.runtime.credentials['serpapi_api_key']
# TODO: search with serpapi
result = SerpAPI(api_key).run(query, result_type=result_type)
if result_type == 'text':
return self.create_text_message(text=result)
return self.create_link_message(link=result)
```
### 参数
工具的整体逻辑都在`_invoke`方法中,这个方法接收两个参数:`user_id``tool_paramters`分别表示用户ID和工具参数
### 返回数据
在工具返回时,你可以选择返回一个消息或者多个消息,这里我们返回一个消息,使用`create_text_message``create_link_message`可以创建一个文本消息或者一个链接消息。
## 5. 准备供应商代码
最后,我们需要在供应商模块下创建一个供应商类,用于实现供应商的凭据验证逻辑,如果凭据验证失败,将会抛出`ToolProviderCredentialValidationError`异常。
`google`模块下创建`google.py`,内容如下。
```python
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolProviderType
from core.tools.tool.tool import Tool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.google.tools.google_search import GoogleSearchTool
from typing import Any, Dict
class GoogleProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: Dict[str, Any]) -> None:
try:
# 1. 此处需要使用GoogleSearchTool()实例化一个GoogleSearchTool它会自动加载GoogleSearchTool的yaml配置但是此时它内部没有凭据信息
# 2. 随后需要使用fork_tool_runtime方法将当前的凭据信息传递给GoogleSearchTool
# 3. 最后invoke即可参数需要根据GoogleSearchTool的yaml中配置的参数规则进行传递
GoogleSearchTool().fork_tool_runtime(
meta={
"credentials": credentials,
}
).invoke(
user_id='',
tool_paramters={
"query": "test",
"result_type": "link"
},
)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))
```
## 完成
当上述步骤完成以后我们就可以在前端看到这个工具了并且可以在Agent中使用这个工具。
当然因为google_search需要一个凭据在使用之前还需要在前端配置它的凭据。
![Alt text](images/index/image-2.png)