mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-04 14:52:12 +08:00
Add 'docs/' from commit '51dddcb3f891d60bc38645e88b2dbd515344da81'
git-subtree-dir: docs git-subtree-mainline:0582d1b3d4git-subtree-split:51dddcb3f8
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: "添加组件"
|
||||
---
|
||||
|
||||
插件由一种或多种组件构成,为 LangBot 提供不同的功能。目前支持的组件类型有:
|
||||
|
||||
- 插件处理器(运行器):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/runner)。
|
||||
- Agent 使用的运行器:为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。
|
||||
- 事件监听器(EventListener):监听流水线执行期间的事件,对上下文或流水线进行修改。
|
||||
- 命令(Command):由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。
|
||||
- 工具(Tool):供 LangBot 内置的 Local Agent 在执行期间由 LLM 调用。
|
||||
- RAG 引擎(KnowledgeEngine):为 LangBot 提供知识库的索引和检索能力,管理文档摄取、检索和删除。
|
||||
- 解析器(Parser):在文档摄取前将 PDF、Word 等二进制文件解析为结构化文本,供 RAG 引擎使用。
|
||||
- 页面(Page):在 LangBot WebUI 侧边栏中注册自定义可视化页面,提供管理面板、数据展示等交互式界面。
|
||||
|
||||
## 添加组件
|
||||
|
||||
在插件目录下执行命令:
|
||||
|
||||
```bash
|
||||
lbp comp <component_type>
|
||||
```
|
||||
|
||||
例如,添加一个事件监听器:
|
||||
|
||||
```bash
|
||||
lbp comp EventListener
|
||||
```
|
||||
|
||||
请根据提示输入组件的配置(若有)。
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > lbp comp EventListener
|
||||
Generating component EventListener...
|
||||
Component EventListener generated successfully.
|
||||
组件 EventListener 生成成功。
|
||||
➜ HelloPlugin >
|
||||
```
|
||||
|
||||
<Info title="">
|
||||
CLI 将在插件目录下生成一个 `components` 目录,并在其中创建组件对应目录
|
||||
```
|
||||
.
|
||||
├── assets
|
||||
│ └── icon.svg
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ └── event_listener
|
||||
│ ├── __init__.py
|
||||
│ ├── default.py
|
||||
│ └── default.yaml
|
||||
├── main.py
|
||||
├── manifest.yaml
|
||||
├── README.md
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
同时,会在插件的`manifest.yaml`中添加组件发现配置:
|
||||
|
||||
```yaml
|
||||
...
|
||||
components:
|
||||
EventListener:
|
||||
fromDirs:
|
||||
- path: components/event_listener/
|
||||
...
|
||||
```
|
||||
如需删除组件,可自行删除对应信息。
|
||||
</Info>
|
||||
|
||||
|
||||
## 组件类型
|
||||
|
||||
各个组件的详细使用方式请参考:
|
||||
|
||||
- [运行器](./runner)
|
||||
- [EventListener](./event-listener)
|
||||
- [Command](./command)
|
||||
- [Tool](./tool)
|
||||
- [KnowledgeEngine](./knowledge-engine)
|
||||
- [Parser](./parser)
|
||||
- [Page](./page)
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "组件:命令"
|
||||
---
|
||||
|
||||
命令组件由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。如下是命令`!help`的触发示例:
|
||||
|
||||
<img width="600" src="/images/zh/plugin/dev/components/command_use.png" />
|
||||
|
||||
## 添加命令组件
|
||||
|
||||
单个插件中能添加任意数量的命令,请在插件目录执行命令`lbp comp Command`,并根据提示输入命令的配置。
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > lbp comp Command
|
||||
Generating component Command...
|
||||
Command name: info
|
||||
Command description: Show information of the query
|
||||
Component Command generated successfully.
|
||||
组件 Command 生成成功。
|
||||
```
|
||||
|
||||
现在即会在`components/commands/`目录下生成`info.yaml`和`info.py`文件,`.yaml`定义了`!info`命令的基础信息,`.py`是该命令的处理程序:
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > tree
|
||||
...
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ ├── commands
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── info.py
|
||||
│ │ └── info.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 清单文件:命令组件
|
||||
|
||||
```yaml
|
||||
apiVersion: v1 # 请勿修改
|
||||
kind: Command # 请勿修改
|
||||
metadata:
|
||||
name: info # 命令名称,在使用时,用户会通过 !info 触发该命令
|
||||
label: # 命令显示名称,用于显示在 LangBot 的 UI 上,支持多语言
|
||||
en_US: Info
|
||||
zh_Hans: Info
|
||||
description: # 命令描述,用于显示在 LangBot 的 UI 上,支持多语言。可选。
|
||||
en_US: 'Show information of the query'
|
||||
zh_Hans: '发送此次消息的详细信息'
|
||||
spec:
|
||||
execution:
|
||||
python:
|
||||
path: info.py # 命令处理程序,请勿修改
|
||||
attr: Info # 命令处理程序的类名,与 info.py 中的类名一致
|
||||
```
|
||||
|
||||
## 插件处理
|
||||
|
||||
默认会生成如下代码(`components/command/<命令名称>.py`),您需要在`Info`类的`initialize`方法中注册并实现子命令的处理逻辑。
|
||||
|
||||
```python
|
||||
# Auto generated by LangBot Plugin SDK.
|
||||
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
from langbot_plugin.api.definition.components.command.command import Command, Subcommand
|
||||
from langbot_plugin.api.entities.builtin.command.context import ExecuteContext, CommandReturn
|
||||
|
||||
|
||||
class Info(Command):
|
||||
|
||||
async def initialize(self):
|
||||
await super().initialize()
|
||||
|
||||
"Fill with your code here"
|
||||
```
|
||||
|
||||
添加子命令:
|
||||
|
||||
```python
|
||||
...
|
||||
class Info(Command):
|
||||
|
||||
async def initialize(self):
|
||||
await super().initialize()
|
||||
|
||||
@self.subcommand(
|
||||
name="", # 空字符串表示根命令
|
||||
help="Show information of the query", # 命令帮助信息
|
||||
usage="info", # 命令使用示例,显示在命令帮助信息中
|
||||
aliases=["i"], # 命令别名
|
||||
)
|
||||
async def send(self, context: ExecuteContext) -> AsyncGenerator[CommandReturn, None]:
|
||||
print(context)
|
||||
|
||||
reply_text = f"Query ID: {context.query_id}\n"
|
||||
reply_text += f"command: {context.command}\n"
|
||||
reply_text += f"command_text: {context.command_text}\n"
|
||||
reply_text += f"params: {context.params}\n"
|
||||
reply_text += f"crt_params: {context.crt_params}\n"
|
||||
reply_text += f"privilege: {context.privilege}\n"
|
||||
reply_text += f"session: {context.session.launcher_type.value}_{context.session.launcher_id}\n"
|
||||
|
||||
yield CommandReturn(
|
||||
text=reply_text,
|
||||
)
|
||||
|
||||
@self.subcommand(
|
||||
name="field",
|
||||
help="Show information of the field",
|
||||
usage="info field",
|
||||
aliases=["f"],
|
||||
)
|
||||
async def field(self, context: ExecuteContext) -> AsyncGenerator[CommandReturn, None]:
|
||||
print(context)
|
||||
|
||||
field_name = context.crt_params[0]
|
||||
field_value = getattr(context, field_name)
|
||||
|
||||
yield CommandReturn(
|
||||
text=f"{field_name}: {field_value}",
|
||||
)
|
||||
```
|
||||
|
||||
该代码中,`send`函数通过装饰器`@self.subcommand`注册为子命令,并在其中打印了命令的上下文(ExecuteContext)信息并拼接成一条回复消息。
|
||||
|
||||
### 子命令注册
|
||||
|
||||
`name` 为子命令名称,留空表示处理主命令。不为空则匹配第二个参数为命令,例如:
|
||||
|
||||
- `!info` 匹配 name="" 的子命令
|
||||
- `!info field` 匹配 name="field" 的子命令
|
||||
- `!info field value` 匹配 name="field" 的子命令,`value` 为子命令参数
|
||||
- 特殊地,`name="*"` 匹配所有未匹配的一级子命令,并将`info`后的每一节作为参数传递传递参数,例如`!info 123`, `!info abc`. 此时可以从`context.crt_params`中获取到`['123']`或`['abc']`,取决于用户输入的参数。
|
||||
|
||||
子命令函数中,可通过 `context` 变量读取到命令参数。
|
||||
|
||||
上述命令效果如图:
|
||||
|
||||
<img width="600" src="/images/zh/plugin/dev/components/info_command_demo.png" />
|
||||
|
||||
### 命令上下文
|
||||
|
||||
```python
|
||||
|
||||
class ExecuteContext(pydantic.BaseModel):
|
||||
"""单次命令执行上下文"""
|
||||
|
||||
query_id: int
|
||||
"""请求ID"""
|
||||
|
||||
session: provider_session.Session
|
||||
"""本次消息所属的会话对象"""
|
||||
|
||||
command_text: str
|
||||
"""命令完整文本"""
|
||||
|
||||
command: str
|
||||
"""命令名称"""
|
||||
|
||||
crt_command: str
|
||||
"""当前命令
|
||||
|
||||
多级命令中crt_command为当前命令,command为根命令。
|
||||
例如:!plugin on Webwlkr
|
||||
处理到plugin时,command为plugin,crt_command为plugin
|
||||
处理到on时,command为plugin,crt_command为on
|
||||
"""
|
||||
|
||||
params: list[str]
|
||||
"""命令参数
|
||||
|
||||
整个命令以空格分割后的参数列表
|
||||
"""
|
||||
|
||||
crt_params: list[str]
|
||||
"""当前命令参数
|
||||
|
||||
多级命令中crt_params为当前命令参数,params为根命令参数。
|
||||
例如:!plugin on Webwlkr
|
||||
处理到plugin时,params为['on', 'Webwlkr'],crt_params为['on', 'Webwlkr']
|
||||
处理到on时,params为['on', 'Webwlkr'],crt_params为['Webwlkr']
|
||||
"""
|
||||
|
||||
privilege: int
|
||||
"""会话权限级别"""
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### 命令返回值
|
||||
|
||||
命令返回值(CommandReturn)目前支持返回文本(text)和图片(image_url,图片链接)、错误(error)。
|
||||
|
||||
```python
|
||||
yield CommandReturn(
|
||||
text=reply_text,
|
||||
)
|
||||
|
||||
yield CommandReturn(
|
||||
image_url=image_url,
|
||||
)
|
||||
```
|
||||
|
||||
命令处理支持返回多条消息,故请使用`yield`语句返回消息。
|
||||
|
||||
具体的返回值请参考`CommandReturn`的定义:[langbot_plugin.api.entities.builtin.command.context.CommandReturn](https://github.com/langbot-app/langbot-plugin-sdk/tree/main/src/langbot_plugin/api/entities/builtin/command/context.py)
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
一个命令会被**多个会话并发触发**:
|
||||
|
||||
- 配置每次调用读取,不要在 `initialize()` 中读取后缓存到实例。
|
||||
- 不要把命令参数、返回值或会话数据留在实例字段或模块级变量上;`ExecuteContext` 只在本次执行内有效。
|
||||
- 并发触发意味着实例字段会被交织写入,不要用它保存「当前会话」之类的状态;需要跨调用保留的数据放 Host 存储,键包含会话身份。
|
||||
- 如果按安装维护了进程内缓存,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 接下来做什么
|
||||
|
||||
您已经了解了命令注册和命令执行的基本信息,接下来可以:
|
||||
|
||||
- 查看完整的 [`ExecuteContext` 上下文 API](/zh/plugin/dev/apis/agent-run#command)
|
||||
- 查看 [LangBot API](/zh/plugin/dev/apis/common)
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: "组件:事件监听器"
|
||||
---
|
||||
|
||||
<Info>
|
||||
本组件用于扩展流水线的处理流程。如果您需要处理机器人收到的消息、成员入群等事件,请参阅[事件处理器](/zh/plugin/dev/components/runner)。
|
||||
</Info>
|
||||
|
||||
LangBot 的流水线在运行期间会产生一些事件,供插件 Hook 并处理。每个插件中只允许添加一个事件监听器(components/event_listener/default.yaml),但在其中可以注册任意数量的事件。
|
||||
|
||||
## 添加事件监听器组件
|
||||
|
||||
请在插件目录执行命令`lbp comp EventListener`,创建事件监听器组件不需要填写任何配置。
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > lbp comp EventListener
|
||||
Generating component EventListener...
|
||||
Component EventListener generated successfully.
|
||||
组件 EventListener 生成成功。
|
||||
```
|
||||
|
||||
现在即会在`components/event_listener/`目录下生成`default.yaml`和`default.py`文件,`.yaml`定义了事件监听器的基础信息,`.py`是该事件监听器的处理程序:
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > tree
|
||||
...
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ └── event_listener
|
||||
│ ├── __init__.py
|
||||
│ ├── default.py
|
||||
│ └── default.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 清单文件:事件监听器
|
||||
|
||||
由于每个插件仅允许添加一个事件监听器,故清单文件中不需要修改任何内容。
|
||||
|
||||
## 插件处理
|
||||
|
||||
默认会生成如下代码(components/event_listener/default.py),您需要在`DefaultEventListener`类的`initialize`方法中注册并实现事件的处理逻辑。
|
||||
|
||||
```python
|
||||
# Auto generated by LangBot Plugin SDK.
|
||||
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot_plugin.api.definition.components.common.event_listener import EventListener
|
||||
from langbot_plugin.api.entities import events, context
|
||||
|
||||
|
||||
class DefaultEventListener(EventListener):
|
||||
|
||||
async def initialize(self):
|
||||
await super().initialize()
|
||||
|
||||
"Fill with your code here"
|
||||
```
|
||||
|
||||
添加对特定事件的监听:
|
||||
|
||||
```python
|
||||
...
|
||||
class DefaultEventListener(EventListener):
|
||||
|
||||
async def initialize(self):
|
||||
await super().initialize()
|
||||
|
||||
@self.handler(events.PersonMessageReceived)
|
||||
async def handler(event_context: context.EventContext):
|
||||
print("Hello LangBot Plugin!")
|
||||
print(event_context)
|
||||
|
||||
await event_context.reply(
|
||||
platform_message.MessageChain([
|
||||
platform_message.Plain(text=f"Hello from Nahida Plugin!"),
|
||||
])
|
||||
)
|
||||
```
|
||||
|
||||
此代码将注册对`PersonMessageReceived(收到来自私聊的任何消息)`事件的监听,并在事件触发时打印事件的上下文(EventContext)信息,并调用事件上下文 API 回复一条消息。
|
||||
|
||||
EventContext 保存了该此事件触发的通用上下文信息,而 EventContext.event 为 PersonMessageReceived (或其他对应的事件类型)的对象,保存了该事件的详细信息。
|
||||
|
||||
```python
|
||||
# EventContext 的定义摘要
|
||||
class EventContext(pydantic.BaseModel):
|
||||
"""事件上下文, 保存此次事件运行的信息"""
|
||||
...
|
||||
|
||||
event: pydantic.SerializeAsAny[BaseEventModel]
|
||||
"""此次事件的对象,具体类型为handler注册时指定监听的类型,可查看events.py中的定义"""
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## 事件注册
|
||||
|
||||
事件注册通过装饰器`@self.handler`实现,装饰器参数为事件类型。可监听的事件均在`langbot_plugin.api.entities.builtin.events`中定义。
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
同一个事件监听器会被**多个工作区、多个事件并发调用**:
|
||||
|
||||
- 不要在实例上按事件累积状态(计数、最近事件、会话映射等),并发回调会互相覆盖。
|
||||
- 回调只从当前 `EventContext` 取数据,不要跨调用复用上下文或其字段。
|
||||
- 需要跨调用保留的数据放 Host 存储,键包含安装标识与事件相关的身份(工作区、会话等)。
|
||||
- 如果按安装维护了进程内状态,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 接下来做什么
|
||||
|
||||
您已经了解了事件监听器的基本信息,接下来可以:
|
||||
|
||||
- 在[流水线事件](/zh/plugin/dev/apis/pipeline-events)中查看可监听的事件及字段
|
||||
- 查看完整的 [`EventContext` 上下文 API](/zh/plugin/dev/apis/agent-run#eventlistener)
|
||||
- 查看 [LangBot API](/zh/plugin/dev/apis/common)
|
||||
@@ -0,0 +1,491 @@
|
||||
---
|
||||
title: "组件:知识引擎"
|
||||
---
|
||||
|
||||
RAG 引擎(KnowledgeEngine)组件允许插件为 LangBot 提供完整的知识库索引与检索能力。用户在 LangBot 中创建知识库时,可以选择由插件提供的 RAG 引擎来管理文档的摄取、检索和删除。或通过插件桥接来自 Dify、RAGFlow、FastGPT 等业内先进知识库服务。
|
||||
|
||||
## 添加 RAG 引擎组件
|
||||
|
||||
单个插件中能添加任意数量的 RAG 引擎,请在插件目录执行命令 `lbp comp KnowledgeEngine`,并根据提示输入 RAG 引擎的配置。
|
||||
|
||||
```bash
|
||||
➜ MyRAGPlugin > lbp comp KnowledgeEngine
|
||||
Generating component KnowledgeEngine...
|
||||
Knowledge Engine name: simple_rag
|
||||
Knowledge Engine description: A simple Knowledge Engine with built-in chunking and retrieval
|
||||
Component KnowledgeEngine generated successfully.
|
||||
组件 KnowledgeEngine 生成成功。
|
||||
```
|
||||
|
||||
现在即会在 `components/knowledge_engine/` 目录下生成 `simple_rag.yaml` 和 `simple_rag.py` 文件,`.yaml` 定义了 RAG 引擎的基础信息和配置 Schema,`.py` 是该引擎的处理程序:
|
||||
|
||||
```bash
|
||||
➜ MyRAGPlugin > tree
|
||||
...
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ └── knowledge_engine
|
||||
│ ├── __init__.py
|
||||
│ ├── simple_rag.py
|
||||
│ └── simple_rag.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 清单文件:RAG 引擎
|
||||
|
||||
```yaml
|
||||
apiVersion: v1 # 请勿修改
|
||||
kind: KnowledgeEngine # 请勿修改
|
||||
metadata:
|
||||
name: simple_rag # RAG 引擎名称,用于标识该引擎
|
||||
label:
|
||||
en_US: Simple Knowledge Engine # 引擎显示名称,用于显示在 LangBot 的 UI 上,支持多语言
|
||||
zh_Hans: 简易 RAG 引擎
|
||||
ja_JP: シンプル RAG エンジン
|
||||
description:
|
||||
en_US: 'A simple Knowledge Engine with built-in chunking and retrieval'
|
||||
zh_Hans: '内置分块和检索能力的简易 RAG 引擎'
|
||||
ja_JP: 'チャンキングと検索を内蔵したシンプルなRAGエンジン'
|
||||
spec:
|
||||
creation_schema: # 用户创建知识库时需要填写的配置参数
|
||||
- name: chunk_size
|
||||
label:
|
||||
en_US: Chunk Size
|
||||
zh_Hans: 分块大小
|
||||
type: integer
|
||||
required: false
|
||||
default: 500
|
||||
- name: chunk_overlap
|
||||
label:
|
||||
en_US: Chunk Overlap
|
||||
zh_Hans: 分块重叠
|
||||
type: integer
|
||||
required: false
|
||||
default: 50
|
||||
retrieval_schema: # 检索时可配置的参数
|
||||
- name: score_threshold
|
||||
label:
|
||||
en_US: Score Threshold
|
||||
zh_Hans: 分数阈值
|
||||
type: float
|
||||
required: false
|
||||
default: 0.5
|
||||
execution:
|
||||
python:
|
||||
path: simple_rag.py # 引擎处理程序,请勿修改
|
||||
attr: SimpleRag # 引擎处理程序的类名,与 simple_rag.py 中的类名一致
|
||||
```
|
||||
|
||||
配置项格式可参考:[插件清单配置项格式](/zh/plugin/dev/basic-info)
|
||||
|
||||
### creation_schema 与 retrieval_schema
|
||||
|
||||
与旧版 `KnowledgeRetriever` 组件使用单一 `spec.config` 不同,KnowledgeEngine 使用两个独立的 Schema:
|
||||
|
||||
- **creation_schema**:用户创建知识库时填写的参数,会在摄取和检索时通过 `creation_settings` 传入插件。
|
||||
- **retrieval_schema**:用户在检索知识库时可调整的参数,通过 `retrieval_settings` 传入插件。
|
||||
|
||||
## 能力声明
|
||||
|
||||
KnowledgeEngine 可以声明自身支持的能力,LangBot 会根据能力声明决定 UI 展示和可用操作:
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.knowledge_engine.engine import KnowledgeEngine, KnowledgeEngineCapability
|
||||
|
||||
class SimpleRag(KnowledgeEngine):
|
||||
|
||||
@classmethod
|
||||
def get_capabilities(cls) -> list[str]:
|
||||
return [
|
||||
KnowledgeEngineCapability.DOC_INGESTION, # 支持文档上传和处理
|
||||
]
|
||||
```
|
||||
|
||||
可用的能力常量:
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| `DOC_INGESTION` | 支持文档上传和处理。声明此能力后,LangBot 将在知识库详情中显示"文档"标签页;若该插件是用于桥接其他的知识库服务或不需要用户上传文档,则可不提供该能力声明。 |
|
||||
| `DOC_PARSING` | 支持原生文档解析(文件到文本的提取)。声明此能力后,用户上传文档时可以选择使用 RAG 引擎内置的解析能力,而不需要外部 [Parser 插件](./parser)。若未声明此能力,则必须安装外部 Parser 插件才能上传文档。 |
|
||||
|
||||
<Info>
|
||||
其他检索行为(如重排序、混合搜索等)由插件的 `retrieval_schema` 控制,不需要能力标志。
|
||||
</Info>
|
||||
|
||||
## 插件处理
|
||||
|
||||
默认会生成如下代码(`components/knowledge_engine/<引擎名称>.py`),您需要实现 `ingest`、`retrieve`、`delete_document` 三个核心方法。完整代码可在 [langbot-plugin-demo](https://github.com/langbot-app/langbot-plugin-demo) 的 `SimpleKnowledgeEngine` 中找到。
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.knowledge_engine.engine import KnowledgeEngine, KnowledgeEngineCapability
|
||||
from langbot_plugin.api.entities.builtin.rag.models import (
|
||||
IngestionContext,
|
||||
IngestionResult,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.rag.context import (
|
||||
RetrievalContext,
|
||||
RetrievalResponse,
|
||||
RetrievalResultEntry,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.rag.enums import DocumentStatus
|
||||
from langbot_plugin.api.entities.builtin.provider.message import ContentElement
|
||||
|
||||
|
||||
class SimpleRag(KnowledgeEngine):
|
||||
|
||||
@classmethod
|
||||
def get_capabilities(cls) -> list[str]:
|
||||
return [KnowledgeEngineCapability.DOC_INGESTION]
|
||||
|
||||
async def on_knowledge_base_create(self, kb_id: str, config: dict) -> None:
|
||||
"""知识库创建时的回调,可用于初始化资源"""
|
||||
pass
|
||||
|
||||
async def on_knowledge_base_delete(self, kb_id: str) -> None:
|
||||
"""知识库删除时的回调,可用于清理资源"""
|
||||
pass
|
||||
|
||||
async def ingest(self, context: IngestionContext) -> IngestionResult:
|
||||
"""将文档摄取到知识库中"""
|
||||
|
||||
# 1. 获取文件内容
|
||||
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
|
||||
|
||||
# 2. 解析文档并分块
|
||||
text = file_bytes.decode('utf-8')
|
||||
chunk_size = context.creation_settings.get('chunk_size', 500)
|
||||
chunk_overlap = context.creation_settings.get('chunk_overlap', 50)
|
||||
chunks = self._split_text(text, chunk_size, chunk_overlap)
|
||||
|
||||
# 3. 调用宿主嵌入模型生成向量
|
||||
embedding_model_uuid = context.creation_settings.get('embedding_model_uuid', '')
|
||||
vectors = await self.plugin.invoke_embedding(embedding_model_uuid, chunks)
|
||||
|
||||
# 4. 写入宿主向量数据库
|
||||
collection_id = context.get_collection_id()
|
||||
ids = [f"{context.file_object.metadata.document_id}_{i}" for i in range(len(chunks))]
|
||||
metadata = [{"document_id": context.file_object.metadata.document_id, "chunk_index": i, "text": chunks[i]} for i in range(len(chunks))]
|
||||
await self.plugin.vector_upsert(collection_id, vectors, ids, metadata)
|
||||
|
||||
return IngestionResult(
|
||||
document_id=context.file_object.metadata.document_id,
|
||||
status=DocumentStatus.COMPLETED,
|
||||
chunks_created=len(chunks),
|
||||
)
|
||||
|
||||
async def retrieve(self, context: RetrievalContext) -> RetrievalResponse:
|
||||
"""从知识库中检索相关内容"""
|
||||
|
||||
# 1. 生成查询向量
|
||||
embedding_model_uuid = context.creation_settings.get('embedding_model_uuid', '')
|
||||
query_vectors = await self.plugin.invoke_embedding(embedding_model_uuid, [context.query])
|
||||
query_vector = query_vectors[0]
|
||||
|
||||
# 2. 向量搜索
|
||||
collection_id = context.get_collection_id()
|
||||
results = await self.plugin.vector_search(collection_id, query_vector, top_k=context.retrieval_settings.get('top_k', 5))
|
||||
|
||||
# 3. 转换为检索结果
|
||||
entries = []
|
||||
for r in results:
|
||||
entry = RetrievalResultEntry(
|
||||
id=r.get('id', ''),
|
||||
content=[ContentElement.from_text(r.get('metadata', {}).get('text', ''))],
|
||||
metadata=r.get('metadata', {}),
|
||||
distance=r.get('score', 0.0),
|
||||
)
|
||||
entries.append(entry)
|
||||
|
||||
return RetrievalResponse(
|
||||
results=entries,
|
||||
total_found=len(entries),
|
||||
metadata={},
|
||||
)
|
||||
|
||||
async def delete_document(self, kb_id: str, document_id: str) -> bool:
|
||||
"""从知识库中删除文档及其关联数据"""
|
||||
collection_id = kb_id
|
||||
deleted = await self.plugin.vector_delete(collection_id, file_ids=[document_id])
|
||||
return deleted > 0
|
||||
```
|
||||
|
||||
### 生命周期钩子
|
||||
|
||||
KnowledgeEngine 提供两个生命周期钩子,会在知识库创建和删除时被调用:
|
||||
|
||||
```python
|
||||
async def on_knowledge_base_create(self, kb_id: str, config: dict) -> None:
|
||||
"""知识库创建时的回调
|
||||
|
||||
Args:
|
||||
kb_id: 知识库 UUID
|
||||
config: 用户在创建时填写的配置(creation_schema 中的字段)
|
||||
"""
|
||||
|
||||
async def on_knowledge_base_delete(self, kb_id: str) -> None:
|
||||
"""知识库删除时的回调
|
||||
|
||||
Args:
|
||||
kb_id: 知识库 UUID
|
||||
"""
|
||||
```
|
||||
|
||||
### 文档摄取
|
||||
|
||||
`ingest` 方法在用户上传文档到知识库时被调用:
|
||||
|
||||
```python
|
||||
async def ingest(self, context: IngestionContext) -> IngestionResult:
|
||||
```
|
||||
|
||||
**IngestionContext** 包含以下信息:
|
||||
|
||||
```python
|
||||
class IngestionContext(pydantic.BaseModel):
|
||||
file_object: FileObject # 待摄取的文件对象
|
||||
knowledge_base_id: str # 目标知识库 ID
|
||||
collection_id: str | None # 向量集合 ID(默认回退到 knowledge_base_id)
|
||||
creation_settings: dict # 知识库创建时的配置参数
|
||||
parsed_content: ParseResult | None # 外部 Parser 插件的预解析内容(若有)
|
||||
```
|
||||
|
||||
**FileObject** 包含文件元信息:
|
||||
|
||||
```python
|
||||
class FileObject(pydantic.BaseModel):
|
||||
metadata: FileMetadata # 包含 filename, file_size, mime_type, document_id, knowledge_base_id 等
|
||||
storage_path: str # 文件在存储系统中的路径
|
||||
```
|
||||
|
||||
**IngestionResult** 应返回摄取结果:
|
||||
|
||||
```python
|
||||
class IngestionResult(pydantic.BaseModel):
|
||||
document_id: str # 文档 ID
|
||||
status: DocumentStatus # 处理状态:COMPLETED / FAILED
|
||||
chunks_created: int = 0 # 创建的分块数量
|
||||
error_message: str | None = None # 失败时的错误信息
|
||||
metadata: dict = {} # 额外元数据
|
||||
```
|
||||
|
||||
### 知识检索
|
||||
|
||||
`retrieve` 方法在知识库被查询时调用:
|
||||
|
||||
```python
|
||||
async def retrieve(self, context: RetrievalContext) -> RetrievalResponse:
|
||||
```
|
||||
|
||||
**RetrievalContext** 包含以下信息:
|
||||
|
||||
```python
|
||||
class RetrievalContext(pydantic.BaseModel):
|
||||
query: str # 查询文本
|
||||
knowledge_base_id: str | None # 知识库 ID
|
||||
collection_id: str | None # 向量集合 ID
|
||||
retrieval_settings: dict # 检索时的配置参数(retrieval_schema 中的字段)
|
||||
creation_settings: dict # 知识库创建时的配置参数
|
||||
filters: dict # 元数据过滤条件(Chroma 风格 where 语法)
|
||||
```
|
||||
|
||||
<Info>
|
||||
LangBot 宿主会在传入 `retrieval_settings` 前自动注入 `top_k` 默认值(默认为 5),插件可通过 `context.retrieval_settings.get('top_k', 5)` 获取。
|
||||
|
||||
`filters` 字段包含从 `retrieval_settings` 中提取的 Chroma 风格 `where` 过滤条件。当调用方在检索设置中提供 `filters`(例如按时间范围、文件类型或自定义元数据字段过滤文档)时,宿主会填充此字段,插件可据此在检索时进行过滤。如果未提供过滤条件,此字段为空 dict。
|
||||
</Info>
|
||||
|
||||
**RetrievalResponse** 应返回检索结果:
|
||||
|
||||
```python
|
||||
class RetrievalResponse(pydantic.BaseModel):
|
||||
results: list[RetrievalResultEntry] # 检索结果列表
|
||||
total_found: int # 匹配的总数
|
||||
metadata: dict # 额外元数据
|
||||
```
|
||||
|
||||
**RetrievalResultEntry** 表示单个检索结果:
|
||||
|
||||
```python
|
||||
class RetrievalResultEntry(pydantic.BaseModel):
|
||||
id: str # 结果 ID
|
||||
content: list[ContentElement] # 内容,使用 ContentElement.from_text() 创建
|
||||
metadata: dict # 元数据
|
||||
distance: float # 距离分数(越小越相关)
|
||||
score: float | None = None # 相似度分数(越大越相关)
|
||||
```
|
||||
|
||||
### 文档删除
|
||||
|
||||
`delete_document` 方法在用户从知识库删除文档时调用:
|
||||
|
||||
```python
|
||||
async def delete_document(self, kb_id: str, document_id: str) -> bool:
|
||||
"""删除文档及其关联数据
|
||||
|
||||
Args:
|
||||
kb_id: 知识库 ID
|
||||
document_id: 文档 ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
```
|
||||
|
||||
## 宿主 RAG API
|
||||
|
||||
KnowledgeEngine 组件可以通过 `self.plugin` 调用 LangBot 宿主提供的 RAG 相关 API,包括嵌入模型调用、向量数据库操作和文件读取。
|
||||
|
||||
### 调用嵌入模型
|
||||
|
||||
```python
|
||||
async def invoke_embedding(
|
||||
self,
|
||||
embedding_model_uuid: str,
|
||||
texts: list[str],
|
||||
) -> list[list[float]]:
|
||||
"""调用宿主的嵌入模型生成向量
|
||||
|
||||
Args:
|
||||
embedding_model_uuid: 嵌入模型 UUID
|
||||
texts: 待嵌入的文本列表
|
||||
|
||||
Returns:
|
||||
向量列表,每个输入文本对应一个向量
|
||||
"""
|
||||
|
||||
# 调用示例
|
||||
vectors = await self.plugin.invoke_embedding("model_uuid", ["Hello", "World"])
|
||||
```
|
||||
|
||||
### 向量写入
|
||||
|
||||
```python
|
||||
async def vector_upsert(
|
||||
self,
|
||||
collection_id: str,
|
||||
vectors: list[list[float]],
|
||||
ids: list[str],
|
||||
metadata: list[dict] | None = None,
|
||||
) -> None:
|
||||
"""将向量写入宿主的向量数据库
|
||||
|
||||
Args:
|
||||
collection_id: 目标集合 ID
|
||||
vectors: 向量列表
|
||||
ids: 向量唯一标识列表
|
||||
metadata: 可选的元数据列表
|
||||
"""
|
||||
|
||||
# 调用示例
|
||||
await self.plugin.vector_upsert(
|
||||
collection_id="kb_uuid",
|
||||
vectors=[[0.1, 0.2, ...], [0.3, 0.4, ...]],
|
||||
ids=["chunk_0", "chunk_1"],
|
||||
metadata=[{"document_id": "doc1"}, {"document_id": "doc1"}],
|
||||
)
|
||||
```
|
||||
|
||||
### 向量搜索
|
||||
|
||||
```python
|
||||
async def vector_search(
|
||||
self,
|
||||
collection_id: str,
|
||||
query_vector: list[float],
|
||||
top_k: int = 5,
|
||||
filters: dict | None = None,
|
||||
) -> list[dict]:
|
||||
"""在宿主的向量数据库中搜索相似向量
|
||||
|
||||
Args:
|
||||
collection_id: 目标集合 ID
|
||||
query_vector: 查询向量
|
||||
top_k: 返回结果数量
|
||||
filters: 可选的元数据过滤条件
|
||||
|
||||
Returns:
|
||||
搜索结果列表(包含 id, score, metadata 等字段)
|
||||
"""
|
||||
|
||||
# 调用示例
|
||||
results = await self.plugin.vector_search(
|
||||
collection_id="kb_uuid",
|
||||
query_vector=[0.1, 0.2, ...],
|
||||
top_k=5,
|
||||
)
|
||||
# 返回格式: [{"id": "chunk_0", "score": 0.123, "metadata": {"document_id": "doc1", ...}}, ...]
|
||||
```
|
||||
|
||||
<Info>
|
||||
`vector_search` 返回的每个结果为 dict,包含 `id`(向量 ID)、`score`(距离分数)和 `metadata`(写入时附带的元数据)三个字段。如果需要在检索结果中返回文本内容,请在摄取阶段将文本存入 metadata 中。
|
||||
</Info>
|
||||
|
||||
### 向量删除
|
||||
|
||||
```python
|
||||
async def vector_delete(
|
||||
self,
|
||||
collection_id: str,
|
||||
file_ids: list[str] | None = None,
|
||||
filters: dict | None = None,
|
||||
) -> int:
|
||||
"""从宿主的向量数据库中删除向量
|
||||
|
||||
Args:
|
||||
collection_id: 目标集合 ID
|
||||
file_ids: 要删除的文件 ID 列表
|
||||
filters: 可选的元数据过滤条件
|
||||
|
||||
Returns:
|
||||
删除的条目数量
|
||||
"""
|
||||
|
||||
# 调用示例
|
||||
deleted = await self.plugin.vector_delete(
|
||||
collection_id="kb_uuid",
|
||||
file_ids=["doc_001"],
|
||||
)
|
||||
```
|
||||
|
||||
<Info>
|
||||
`filters` 参数支持 Chroma 风格的 `where` 语法进行元数据过滤。多个顶层键之间为 AND 关系。支持的运算符:`$eq`、`$ne`、`$gt`、`$gte`、`$lt`、`$lte`、`$in`、`$nin`。示例:`{"file_id": {"$eq": "abc"}}`。
|
||||
|
||||
**注意:** Chroma、Qdrant 和 SeekDB 存储完整的元数据,可以对任意字段进行过滤。Milvus 和 pgvector 仅存储 `text`、`file_id` 和 `chunk_uuid`,对其他字段的过滤将被静默忽略。
|
||||
</Info>
|
||||
|
||||
```python
|
||||
async def get_knowledge_file_stream(self, storage_path: str) -> bytes:
|
||||
"""从宿主存储中获取文件内容
|
||||
|
||||
Args:
|
||||
storage_path: 文件的存储路径(来自 FileObject.storage_path)
|
||||
|
||||
Returns:
|
||||
文件内容的字节数据
|
||||
"""
|
||||
|
||||
# 调用示例
|
||||
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
|
||||
```
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
一个 RAG 引擎会被**多个知识库**同时选用,而平台的存储键 `(实例, 工作区, 组件类型, 插件名, key)` 不含知识库维度:
|
||||
|
||||
- 持久化与缓存的键必须包含**知识库 ID**,否则不同知识库的向量、文档状态会互相覆盖。
|
||||
- 锁与「待对账」状态必须按 `(安装, 知识库)` 记;**一个知识库摄取失败不得阻断同一安装下其他知识库的摄取与检索**。
|
||||
- 只有「已经派发、结果未知」的变更才应进入需要人工对账的状态;确定性的校验错误与派发前的取消不应影响后续调用。
|
||||
- 遥测与诊断上报失败不得影响业务:日志/上报出错不要中断摄取或检索,也不要把租户数据写进进程级单例。
|
||||
- 如果按安装维护了进程内缓存或状态,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 测试 RAG 引擎
|
||||
|
||||
创建完成后,在插件目录执行命令 `lbp run`,启动调试。然后在 LangBot 中:
|
||||
|
||||
1. 进入"知识库"页面
|
||||
2. 点击"创建知识库"
|
||||
3. 选择您的插件提供的 RAG 引擎,并根据引擎的 `creation_schema` 填写配置
|
||||
4. 创建成功后,可上传文档测试摄取能力(如引擎声明了 `DOC_INGESTION` 能力)
|
||||
5. 在流水线中绑定该知识库,测试检索能力
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
title: "组件:知识检索器"
|
||||
---
|
||||
|
||||
知识检索器(Knowledge Retriever)组件允许插件为 LangBot 提供外部知识库检索能力。当用户在 LangBot 中创建外部知识库时,可以选择由插件提供的知识检索器来检索知识。
|
||||
|
||||
<img width="600" src="/images/zh/plugin/dev/components/knowledge_retriever_use.png" />
|
||||
|
||||
## 添加知识检索器组件
|
||||
|
||||
单个插件中能添加任意数量的知识检索器,请在插件目录执行命令`lbp comp KnowledgeRetriever`,并根据提示输入知识检索器的配置。
|
||||
|
||||
```bash
|
||||
➜ FastGPTRetriever > lbp comp KnowledgeRetriever
|
||||
Generating component KnowledgeRetriever...
|
||||
KnowledgeRetriever name: fastgpt
|
||||
KnowledgeRetriever description: Retrieve knowledge from FastGPT knowledge bases
|
||||
Component KnowledgeRetriever generated successfully.
|
||||
组件 KnowledgeRetriever 生成成功。
|
||||
```
|
||||
|
||||
现在即会在`components/knowledge_retriever/`目录下生成`fastgpt.yaml`和`fastgpt.py`文件,`.yaml`定义了知识检索器的基础信息和配置参数,`.py`是该知识检索器的处理程序:
|
||||
|
||||
```bash
|
||||
➜ FastGPTRetriever > tree
|
||||
...
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ └── knowledge_retriever
|
||||
│ ├── __init__.py
|
||||
│ ├── fastgpt.py
|
||||
│ └── fastgpt.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 清单文件:知识检索器
|
||||
|
||||
```yaml
|
||||
apiVersion: v1 # 请勿修改
|
||||
kind: KnowledgeRetriever # 请勿修改
|
||||
metadata:
|
||||
name: fastgpt # 知识检索器名称,用于标识该检索器
|
||||
label:
|
||||
en_US: FastGPT Knowledge Base # 检索器显示名称,用于显示在 LangBot 的 UI 上,支持多语言
|
||||
zh_Hans: FastGPT 知识库
|
||||
ja_JP: FastGPT ナレッジベース
|
||||
description:
|
||||
en_US: 'Retrieve knowledge from FastGPT knowledge bases' # 检索器描述,用于显示在 LangBot 的 UI 上,支持多语言。可选
|
||||
zh_Hans: '从 FastGPT 知识库中检索知识'
|
||||
ja_JP: 'FastGPT ナレッジベースから知識を取得'
|
||||
icon: assets/icon.svg # 检索器图标,会被展示在外部知识库配置页面
|
||||
spec:
|
||||
config: # 检索器配置参数,用户在创建外部知识库时需要填写这些参数
|
||||
- name: api_base_url # 参数名称
|
||||
type: string # 参数类型:string, number, boolean, select
|
||||
label:
|
||||
en_US: API Base URL
|
||||
zh_Hans: API 基础地址
|
||||
ja_JP: API ベース URL
|
||||
description:
|
||||
en_US: 'Base URL for FastGPT API'
|
||||
zh_Hans: 'FastGPT API 基础地址'
|
||||
ja_JP: 'FastGPT API ベース URL'
|
||||
default: 'http://localhost:3000' # 参数默认值
|
||||
required: true # 是否必填
|
||||
- name: api_key
|
||||
type: string
|
||||
label:
|
||||
en_US: FastGPT API Key
|
||||
zh_Hans: FastGPT API Key
|
||||
ja_JP: FastGPT API キー
|
||||
description:
|
||||
en_US: 'API key from your FastGPT instance'
|
||||
zh_Hans: '从您的 FastGPT 实例获取的 API Key'
|
||||
ja_JP: 'FastGPT インスタンスから取得した API Key'
|
||||
default: ''
|
||||
required: true
|
||||
- name: search_mode # select 类型参数示例
|
||||
type: select
|
||||
label:
|
||||
en_US: Search Mode
|
||||
zh_Hans: 搜索模式
|
||||
ja_JP: 検索モード
|
||||
description:
|
||||
en_US: 'The search method to use'
|
||||
zh_Hans: '使用的搜索方法'
|
||||
ja_JP: '使用する検索方法'
|
||||
default: 'embedding'
|
||||
options: # select 类型参数需要定义选项列表
|
||||
- name: 'embedding'
|
||||
label:
|
||||
en_US: 'Embedding Search'
|
||||
zh_Hans: '向量搜索'
|
||||
ja_JP: '埋め込み検索'
|
||||
- name: 'fullTextRecall'
|
||||
label:
|
||||
en_US: 'Full-Text Recall'
|
||||
zh_Hans: '全文检索'
|
||||
ja_JP: '全文検索'
|
||||
- name: using_rerank # boolean 类型参数示例
|
||||
type: boolean
|
||||
label:
|
||||
en_US: Use Re-ranking
|
||||
zh_Hans: 使用重排序
|
||||
ja_JP: リランキングを使用
|
||||
description:
|
||||
en_US: 'Whether to use re-ranking'
|
||||
zh_Hans: '是否使用重排序'
|
||||
ja_JP: 'リランキングを使用するかどうか'
|
||||
default: false
|
||||
required: false
|
||||
execution:
|
||||
python:
|
||||
path: fastgpt.py # 检索器处理程序,请勿修改
|
||||
attr: FastGPT # 检索器处理程序的类名,与 fastgpt.py 中的类名一致
|
||||
```
|
||||
|
||||
配置项格式可参考:[插件清单配置项格式](/zh/plugin/dev/basic-info)
|
||||
|
||||
## 插件处理
|
||||
|
||||
默认会生成如下代码(`components/knowledge_retriever/<检索器名称>.py`),您需要在`FastGPT`类的`retrieve`方法中实现知识检索逻辑。完整代码可在[langbot-plugin-demo](https://github.com/langbot-app/langbot-plugin-demo)中找到。
|
||||
|
||||
```python
|
||||
# Auto generated by LangBot Plugin SDK.
|
||||
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
|
||||
...
|
||||
|
||||
class FastGPT(KnowledgeRetriever):
|
||||
|
||||
async def retrieve(self, context: RetrievalContext) -> list[RetrievalResultEntry]:
|
||||
"""Retrieve knowledge from FastGPT knowledge base"""
|
||||
|
||||
# 1. 获取配置参数
|
||||
api_base_url = self.config.get('api_base_url', 'http://localhost:3000')
|
||||
api_key = self.config.get('api_key')
|
||||
dataset_id = self.config.get('dataset_id')
|
||||
search_mode = self.config.get('search_mode', 'embedding')
|
||||
using_rerank = self.config.get('using_rerank', False)
|
||||
|
||||
# 2. 参数验证
|
||||
if not api_key or not dataset_id:
|
||||
logger.error("Missing required configuration: api_key or dataset_id")
|
||||
return []
|
||||
|
||||
# 3. 构建 API 请求
|
||||
url = f"{api_base_url.rstrip('/')}/api/core/dataset/searchTest"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"datasetId": dataset_id,
|
||||
"text": context.query, # 从 RetrievalContext 获取查询文本
|
||||
"searchMode": search_mode,
|
||||
"usingReRank": using_rerank,
|
||||
}
|
||||
|
||||
try:
|
||||
# 4. 调用外部 API
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=payload, headers=headers, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# 5. 解析响应并转换为 RetrievalResultEntry
|
||||
results = []
|
||||
for record in result.get('data', []):
|
||||
# 合并主要数据和辅助数据作为内容
|
||||
content_text = '\n'.join([
|
||||
record.get('q', ''),
|
||||
record.get('a', '')
|
||||
]).strip()
|
||||
|
||||
# 创建检索结果条目
|
||||
entry = RetrievalResultEntry(
|
||||
id=record.get('id', ''),
|
||||
content=[ContentElement.from_text(content_text)],
|
||||
metadata={
|
||||
'dataset_id': record.get('datasetId', ''),
|
||||
'source_name': record.get('sourceName', ''),
|
||||
'score': record.get('score', 0.0),
|
||||
},
|
||||
# 将相似度分数转换为距离(分数越高,距离越小)
|
||||
distance=1.0 - float(record.get('score', 0.0)),
|
||||
)
|
||||
results.append(entry)
|
||||
|
||||
logger.info(f"Retrieved {len(results)} chunks from FastGPT dataset {dataset_id}")
|
||||
return results
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"HTTP error from FastGPT API: {e.response.status_code} - {e.response.text}")
|
||||
return []
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Request error when calling FastGPT API: {str(e)}")
|
||||
return []
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected error during retrieval: {str(e)}")
|
||||
return []
|
||||
```
|
||||
|
||||
### 检索上下文
|
||||
|
||||
`RetrievalContext` 包含本次检索的上下文信息:
|
||||
|
||||
```python
|
||||
class RetrievalContext(pydantic.BaseModel):
|
||||
"""知识检索上下文"""
|
||||
|
||||
query: str
|
||||
"""查询文本,用户的检索问题"""
|
||||
```
|
||||
|
||||
### 检索结果
|
||||
|
||||
检索结果需要转换为 `RetrievalResultEntry` 对象列表,每个对象代表一个检索到的知识块:
|
||||
|
||||
```python
|
||||
class RetrievalResultEntry(pydantic.BaseModel):
|
||||
"""单个检索结果条目"""
|
||||
|
||||
id: str
|
||||
"""结果 ID,唯一标识此条目"""
|
||||
|
||||
content: list[ContentElement]
|
||||
"""结果内容,使用 ContentElement.from_text() 创建文本内容"""
|
||||
|
||||
metadata: dict[str, Any]
|
||||
"""结果元数据,可以包含任意键值对,如来源、分数等"""
|
||||
|
||||
distance: float
|
||||
"""距离分数,表示与查询的相关度(越小越相关)
|
||||
通常通过 1.0 - similarity_score 计算"""
|
||||
```
|
||||
|
||||
### 获取配置参数
|
||||
|
||||
通过 `self.config.get(参数名, 默认值)` 获取用户配置的参数值:
|
||||
|
||||
```python
|
||||
# 获取字符串参数
|
||||
api_base_url = self.config.get('api_base_url', 'http://localhost:3000')
|
||||
|
||||
# 获取数字参数
|
||||
limit = self.config.get('limit', 5000)
|
||||
similarity = self.config.get('similarity', 0.0)
|
||||
|
||||
# 获取布尔参数
|
||||
using_rerank = self.config.get('using_rerank', False)
|
||||
|
||||
# 获取 select 参数
|
||||
search_mode = self.config.get('search_mode', 'embedding')
|
||||
```
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
一个知识检索器会被**多个知识库**选用,平台的存储键 `(实例, 工作区, 组件类型, 插件名, key)` 不含知识库维度:
|
||||
|
||||
- 检索所需的缓存、索引或中间结果必须按**知识库 ID** 隔离,键里要带上知识库 ID,不能放到实例上。
|
||||
- 检索的锁与「待对账」状态按 `(安装, 知识库)` 记,一个知识库检索失败不得让同一安装下其他知识库的检索不可用。
|
||||
- `RetrievalContext` 只在单次检索内有效,不要把它或检索结果保存到实例字段跨调用复用。
|
||||
- 需要跨调用保留的知识库数据存到 Host 存储,键同时包含安装标识与知识库 ID。
|
||||
- 如果按安装缓存了连接、索引或其他进程内状态,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 测试检索器
|
||||
|
||||
创建完成后,在插件目录执行命令 `lbp run`,启动调试。然后在 LangBot 中:
|
||||
|
||||
1. 进入"知识库"页面
|
||||
2. 添加外部知识库
|
||||
3. 选择您的插件提供的知识检索器并填写配置
|
||||
|
||||
<img width="600" src="/images/zh/plugin/dev/components/knowledge_retriever_config.png" />
|
||||
|
||||
保存后即可在 LangBot 流水线中选用该知识库。
|
||||
@@ -0,0 +1,255 @@
|
||||
---
|
||||
title: "组件:页面"
|
||||
---
|
||||
|
||||
Page 组件允许插件在 LangBot 的 WebUI 侧边栏中注册自定义的可视化页面。页面运行在 iframe 沙箱中,通过 Page SDK 与宿主通信,并可调用插件后端 API。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- **管理面板**:为插件提供可视化的配置和数据管理界面(如 FAQ 管理、数据统计仪表盘)
|
||||
- **数据展示**:展示插件运行时的统计数据、日志、图表等
|
||||
- **交互式工具**:提供表单、编辑器等交互式界面
|
||||
|
||||
<img width="600" src="/images/zh/plugin/dev/components/plugin-page-demo.png" />
|
||||
|
||||
## 添加页面组件
|
||||
|
||||
在插件目录执行命令:
|
||||
|
||||
```bash
|
||||
lbp comp Page
|
||||
```
|
||||
|
||||
根据提示输入页面名称后,会在 `components/pages/` 目录下生成页面文件:
|
||||
|
||||
```bash
|
||||
├── components
|
||||
│ └── pages
|
||||
│ └── dashboard
|
||||
│ ├── dashboard.yaml # 页面清单
|
||||
│ ├── dashboard.py # 页面后端处理程序
|
||||
│ ├── index.html # 页面入口
|
||||
│ └── i18n # 翻译文件(可选)
|
||||
│ ├── en_US.json
|
||||
│ └── zh_Hans.json
|
||||
```
|
||||
|
||||
同时,`manifest.yaml` 中会添加组件发现配置:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
components:
|
||||
Page:
|
||||
fromDirs:
|
||||
- path: components/pages/
|
||||
maxDepth: 2
|
||||
```
|
||||
|
||||
## 清单文件:页面
|
||||
|
||||
```yaml
|
||||
apiVersion: v1 # 请勿修改
|
||||
kind: Page # 请勿修改
|
||||
metadata:
|
||||
name: dashboard # 页面唯一 ID,同一插件内不可重复
|
||||
label:
|
||||
en_US: Dashboard # 页面显示名称,显示在 WebUI 侧边栏,支持多语言
|
||||
zh_Hans: 仪表盘
|
||||
spec:
|
||||
path: index.html # HTML 入口文件,相对于该 YAML 所在目录的路径
|
||||
execution:
|
||||
python:
|
||||
path: dashboard.py # 页面后端处理程序
|
||||
attr: DashboardPage # 处理程序类名
|
||||
```
|
||||
|
||||
## 后端处理
|
||||
|
||||
Page 组件的后端处理程序继承 `Page` 基类,实现 `handle_api` 方法来处理来自前端页面的 API 请求。
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse
|
||||
|
||||
|
||||
class DashboardPage(Page):
|
||||
|
||||
async def handle_api(self, request: PageRequest) -> PageResponse:
|
||||
# request.endpoint: API 端点路径,如 '/stats'
|
||||
# request.method: HTTP 方法(GET、POST、PUT、DELETE)
|
||||
# request.body: 请求体(已解析的 JSON,或 None)
|
||||
|
||||
if request.endpoint == '/stats' and request.method == 'GET':
|
||||
# 从 Host 存储读取当前 installation 的数据
|
||||
entries = await self.load_entries_for_current_installation()
|
||||
return PageResponse.ok({'total': len(entries)})
|
||||
|
||||
return PageResponse.fail(f'Unknown endpoint: {request.endpoint}')
|
||||
```
|
||||
|
||||
### PageRequest 字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `endpoint` | `str` | API 端点路径(如 `'/entries'`) |
|
||||
| `method` | `str` | HTTP 方法(`GET`、`POST`、`PUT`、`DELETE`) |
|
||||
| `body` | `Any` | 请求体(已解析的 JSON,或 `None`) |
|
||||
|
||||
### PageResponse 构造
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `PageResponse.ok(data)` | 成功响应,`data` 为任意可 JSON 序列化的数据 |
|
||||
| `PageResponse.fail(error)` | 失败响应,`error` 为错误信息字符串 |
|
||||
|
||||
## 前端页面开发
|
||||
|
||||
在 HTML 文件中引入 Page SDK,即可与插件后端通信:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
background: var(--langbot-bg, #ffffff);
|
||||
color: var(--langbot-text, #0a0a0a);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 data-i18n="title">Dashboard</h1>
|
||||
<p id="stats"></p>
|
||||
|
||||
<script src="/api/v1/plugins/_sdk/page-sdk.js"></script>
|
||||
<script>
|
||||
langbot.onReady(async function(ctx) {
|
||||
// 调用后端 API
|
||||
var data = await langbot.api('/stats', null, 'GET');
|
||||
document.getElementById('stats').textContent = 'Total: ' + data.total;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Page SDK API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `langbot.onReady(callback)` | SDK 就绪后触发回调,`callback` 接收 `ctx` 参数(包含 `theme` 和 `language`) |
|
||||
| `langbot.api(endpoint, body?, method?)` | 调用插件后端 `handle_api`,返回 Promise |
|
||||
| `langbot.t(key, fallback?)` | 获取翻译字符串 |
|
||||
| `langbot.onThemeChange(callback)` | 主题变更回调 |
|
||||
| `langbot.onLanguageChange(callback)` | 语言变更回调 |
|
||||
| `langbot.applyI18n()` | 手动重新应用 `data-i18n` 翻译 |
|
||||
|
||||
### 暗色模式
|
||||
|
||||
SDK 会自动在页面上设置 CSS 自定义属性,直接使用即可:
|
||||
|
||||
| CSS 变量 | 用途 |
|
||||
|------|------|
|
||||
| `--langbot-bg` | 页面背景色 |
|
||||
| `--langbot-bg-card` | 卡片背景色 |
|
||||
| `--langbot-text` | 主文字色 |
|
||||
| `--langbot-text-muted` | 次要文字色 |
|
||||
| `--langbot-border` | 边框色 |
|
||||
| `--langbot-accent` | 强调色 |
|
||||
|
||||
### 页面 i18n
|
||||
|
||||
在页面目录下创建 `i18n/` 目录,放入 JSON 翻译文件:
|
||||
|
||||
```
|
||||
pages/dashboard/
|
||||
├── index.html
|
||||
└── i18n/
|
||||
├── en_US.json
|
||||
└── zh_Hans.json
|
||||
```
|
||||
|
||||
翻译文件格式为扁平 JSON 键值对:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "仪表盘",
|
||||
"totalEntries": "条目总数"
|
||||
}
|
||||
```
|
||||
|
||||
在 HTML 元素上添加 `data-i18n` 属性,SDK 会自动替换文本:
|
||||
|
||||
```html
|
||||
<h1 data-i18n="title">Dashboard</h1>
|
||||
```
|
||||
|
||||
## 完整示例:FAQ 管理器
|
||||
|
||||
以下是一个完整的 Page 组件示例,实现了 FAQ 条目的增删改查。
|
||||
|
||||
**后端处理程序**(`components/pages/manager/manager.py`):
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse
|
||||
|
||||
|
||||
class ManagerPage(Page):
|
||||
|
||||
async def handle_api(self, request: PageRequest) -> PageResponse:
|
||||
if request.endpoint == '/entries' and request.method == 'GET':
|
||||
entries = await self.load_entries_for_current_installation()
|
||||
return PageResponse.ok({'entries': entries})
|
||||
|
||||
if request.endpoint == '/entries' and request.method == 'POST':
|
||||
question = (request.body or {}).get('question', '').strip()
|
||||
answer = (request.body or {}).get('answer', '').strip()
|
||||
if not question or not answer:
|
||||
return PageResponse.fail('question and answer are required')
|
||||
entry = {'question': question, 'answer': answer}
|
||||
await self.save_entry_for_current_installation(entry)
|
||||
return PageResponse.ok({'entry': entry})
|
||||
|
||||
if request.endpoint == '/entries' and request.method == 'DELETE':
|
||||
entry_id = (request.body or {}).get('id', '')
|
||||
if await self.delete_entry_for_current_installation(entry_id):
|
||||
return PageResponse.ok({'deleted': entry_id})
|
||||
return PageResponse.fail('entry not found')
|
||||
|
||||
return PageResponse.fail(f'Unknown: {request.method} {request.endpoint}')
|
||||
```
|
||||
|
||||
**前端页面**(`components/pages/manager/index.html`)中通过 `langbot.api()` 调用后端:
|
||||
|
||||
```javascript
|
||||
// 加载条目
|
||||
var data = await langbot.api('/entries', null, 'GET');
|
||||
|
||||
// 添加条目
|
||||
await langbot.api('/entries', { question: '...', answer: '...' }, 'POST');
|
||||
|
||||
// 删除条目
|
||||
await langbot.api('/entries', { id: '...' }, 'DELETE');
|
||||
```
|
||||
|
||||
完整示例代码请参考 [FAQManager 插件](https://github.com/langbot-app/langbot-plugin-demo/tree/main/FAQManager)。
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
同一个 Page 会被**多次页面请求**调用(同一安装下的不同请求、不同工作区):
|
||||
|
||||
- 不要把上一次请求的数据留在实例上;端点、方法、body 与当前安装信息一律从当前 `PageRequest` / 绑定取。
|
||||
- 页面数据一律按安装作用域存放(installation-scoped Host 存储);`self.plugin` 是进程级共享对象,不能保存租户数据。
|
||||
- 不同工作区的请求会共享同一个 Page 实例,实例字段会被交织使用;需要跨请求保留的数据放 Host 存储。
|
||||
- 如果按安装维护了进程内缓存,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 页面运行在 `sandbox="allow-scripts allow-forms"` 的 iframe 中,不能弹窗或导航父页面
|
||||
- 认证 Page 必须把租户数据放在 installation-scoped Host 存储中;`self.plugin` 是进程级共享对象,不能保存租户条目。上面的存储 helper 名仅为示意,请使用当前组件可用的 SDK Host 存储 API。
|
||||
- 使用 `PageResponse.ok()` 和 `PageResponse.fail()` 构造响应,确保格式一致
|
||||
- Page SDK 的 `<script>` 标签必须放在使用 `langbot` 对象的代码之前
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
title: "组件:解析器"
|
||||
---
|
||||
|
||||
解析器(Parser)组件允许插件为 LangBot 提供文档解析能力。在用户上传文档到知识库时,LangBot 会在 RAG 引擎摄取(`ingest`)之前调用解析器,将 PDF、Word、Markdown 等二进制文件提取为结构化文本。
|
||||
|
||||
Parser 与 KnowledgeEngine 的关系:
|
||||
- **Parser** 负责将文件转换为文本(file → text)
|
||||
- **KnowledgeEngine** 负责将文本索引和检索(text → chunks → vectors)
|
||||
|
||||
如果 RAG 引擎自身已具备文档解析能力(声明了 `DOC_PARSING` 能力),用户可以选择使用 RAG 引擎内置的解析能力,也可以选择使用外部 Parser 插件。
|
||||
|
||||
## 添加解析器组件
|
||||
|
||||
单个插件中能添加任意数量的解析器,请在插件目录执行命令 `lbp comp Parser`,并根据提示输入解析器的配置。
|
||||
|
||||
```bash
|
||||
➜ MyParserPlugin > lbp comp Parser
|
||||
Generating component Parser...
|
||||
Parser name: pdf_parser
|
||||
Parser description: A PDF document parser
|
||||
Component Parser generated successfully.
|
||||
组件 Parser 生成成功。
|
||||
```
|
||||
|
||||
现在即会在 `components/parser/` 目录下生成 `pdf_parser.yaml` 和 `pdf_parser.py` 文件,`.yaml` 定义了解析器的基础信息和支持的 MIME 类型,`.py` 是该解析器的处理程序:
|
||||
|
||||
```bash
|
||||
➜ MyParserPlugin > tree
|
||||
...
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ └── parser
|
||||
│ ├── __init__.py
|
||||
│ ├── pdf_parser.py
|
||||
│ └── pdf_parser.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 清单文件:解析器
|
||||
|
||||
```yaml
|
||||
apiVersion: v1 # 请勿修改
|
||||
kind: Parser # 请勿修改
|
||||
metadata:
|
||||
name: pdf_parser # 解析器名称,用于标识该解析器
|
||||
label:
|
||||
en_US: PDF Parser # 解析器显示名称,用于显示在 LangBot 的 UI 上,支持多语言
|
||||
zh_Hans: PDF 解析器
|
||||
description:
|
||||
en_US: 'A PDF document parser'
|
||||
zh_Hans: 'PDF 文档解析器'
|
||||
spec:
|
||||
supported_mime_types: # 声明支持的文件 MIME 类型
|
||||
- application/pdf
|
||||
execution:
|
||||
python:
|
||||
path: pdf_parser.py # 解析器处理程序,请勿修改
|
||||
attr: PdfParser # 解析器处理程序的类名,与 pdf_parser.py 中的类名一致
|
||||
```
|
||||
|
||||
### supported_mime_types
|
||||
|
||||
`supported_mime_types` 声明该解析器支持解析的文件类型。常见 MIME 类型:
|
||||
|
||||
| MIME 类型 | 说明 |
|
||||
|----------|------|
|
||||
| `application/pdf` | PDF 文档 |
|
||||
| `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | Word 文档(.docx) |
|
||||
| `text/markdown` | Markdown 文件 |
|
||||
| `text/plain` | 纯文本文件 |
|
||||
| `text/html` | HTML 文件 |
|
||||
|
||||
## 插件处理
|
||||
|
||||
默认会生成如下代码(`components/parser/<解析器名称>.py`),您需要实现 `parse` 方法。
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.parser.parser import Parser
|
||||
from langbot_plugin.api.entities.builtin.rag.models import (
|
||||
ParseContext,
|
||||
ParseResult,
|
||||
TextSection,
|
||||
)
|
||||
|
||||
|
||||
class PdfParser(Parser):
|
||||
"""解析器组件,用于从文件中提取文本。"""
|
||||
|
||||
async def parse(self, context: ParseContext) -> ParseResult:
|
||||
"""解析文件并提取结构化文本。
|
||||
|
||||
Args:
|
||||
context: 包含 file_content(bytes)、mime_type、filename 和 metadata。
|
||||
|
||||
Returns:
|
||||
ParseResult,包含提取的文本和可选的结构化分节。
|
||||
"""
|
||||
# TODO: 实现解析逻辑
|
||||
text = context.file_content.decode("utf-8", errors="replace")
|
||||
|
||||
return ParseResult(
|
||||
text=text,
|
||||
sections=[
|
||||
TextSection(
|
||||
content=text,
|
||||
heading=context.filename,
|
||||
level=0,
|
||||
),
|
||||
],
|
||||
metadata={
|
||||
"filename": context.filename,
|
||||
"mime_type": context.mime_type,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### 解析方法
|
||||
|
||||
`parse` 方法在文档上传到知识库时被调用(在 RAG 引擎摄取之前):
|
||||
|
||||
```python
|
||||
async def parse(self, context: ParseContext) -> ParseResult:
|
||||
```
|
||||
|
||||
**ParseContext** 包含以下信息:
|
||||
|
||||
```python
|
||||
class ParseContext(pydantic.BaseModel):
|
||||
file_content: bytes # 原始文件字节(由 LangBot 从存储中读取)
|
||||
mime_type: str # 检测到的文件 MIME 类型
|
||||
filename: str # 原始文件名
|
||||
metadata: dict[str, Any] # FileObject 中的额外元数据
|
||||
```
|
||||
|
||||
**ParseResult** 应返回解析结果:
|
||||
|
||||
```python
|
||||
class ParseResult(pydantic.BaseModel):
|
||||
text: str # 提取的完整纯文本
|
||||
sections: list[TextSection] = [] # 结构化分节(可选)
|
||||
metadata: dict[str, Any] = {} # 解析元数据(如 page_count、language 等)
|
||||
```
|
||||
|
||||
**TextSection** 表示文档中的一个段落/分节:
|
||||
|
||||
```python
|
||||
class TextSection(pydantic.BaseModel):
|
||||
content: str # 分节文本内容
|
||||
heading: str | None = None # 分节标题
|
||||
level: int = 0 # 嵌套层级
|
||||
page: int | None = None # 来源页码(适用于 PDF 等)
|
||||
metadata: dict[str, Any] = {} # 额外分节元数据
|
||||
```
|
||||
|
||||
## 与 KnowledgeEngine 的协作
|
||||
|
||||
当用户上传文档时,LangBot 会根据以下逻辑决定解析流程:
|
||||
|
||||
- 若用户选择了外部 Parser 插件,LangBot 会先调用 Parser 的 `parse` 方法,然后将解析结果通过 `IngestionContext.parsed_content` 传递给 RAG 引擎的 `ingest` 方法。
|
||||
- 若 RAG 引擎声明了 `DOC_PARSING` 能力且用户未选择外部解析器,则由 RAG 引擎自行处理文档解析。
|
||||
|
||||
KnowledgeEngine 可通过检查 `IngestionContext.parsed_content` 来判断是否存在预解析内容:
|
||||
|
||||
```python
|
||||
async def ingest(self, context: IngestionContext) -> IngestionResult:
|
||||
if context.parsed_content:
|
||||
# 使用外部 Parser 的解析结果
|
||||
text = context.parsed_content.text
|
||||
sections = context.parsed_content.sections
|
||||
else:
|
||||
# 自行解析文档
|
||||
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
|
||||
text = file_bytes.decode('utf-8')
|
||||
...
|
||||
```
|
||||
|
||||
## 跨插件调用解析器
|
||||
|
||||
在调用其他插件提供的解析器之前,可以先通过 `self.plugin.list_parsers` 枚举当前可用的解析器:
|
||||
|
||||
```python
|
||||
parsers = await self.plugin.list_parsers(mime_type="application/pdf")
|
||||
# 返回的每项包含 plugin_id、plugin_author、plugin_name、name、description、supported_mime_types
|
||||
```
|
||||
|
||||
若返回空列表,说明宿主当前没有已连接且支持该 MIME 类型的 Parser 插件。
|
||||
|
||||
拿到 `plugin_author` 和 `plugin_name` 后,即可通过 `self.plugin.invoke_parser` 调用对应解析器:
|
||||
|
||||
```python
|
||||
parser = parsers[0]
|
||||
result = await self.plugin.invoke_parser(
|
||||
plugin_author=parser["plugin_author"],
|
||||
plugin_name=parser["plugin_name"],
|
||||
storage_path=context.file_object.storage_path,
|
||||
mime_type=context.file_object.metadata.mime_type,
|
||||
filename=context.file_object.metadata.filename,
|
||||
metadata={},
|
||||
)
|
||||
# result 为 dict,包含 text、sections、metadata
|
||||
```
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
一个 Parser 会被**多个知识库**选用(同一安装下不同知识库会上传不同 MIME 类型的文件),而平台的存储键不含知识库维度:
|
||||
|
||||
- 解析缓存与临时文件必须按**知识库 ID** 隔离,键里要带上知识库 ID,不要缓存到实例上。
|
||||
- `ParseContext` 与解析中间结果只在单次解析内有效,不要保存到实例字段或模块级变量。
|
||||
- 解析失败只影响当前知识库的这次上传,不要让其他知识库的解析不可用。
|
||||
- 如果按安装缓存了模型、解析器或临时目录,请在 `on_installation_revoked(binding)` 中释放,并确保临时文件可被清理。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 测试解析器
|
||||
|
||||
创建完成后,在插件目录执行命令 `lbp run`,启动调试。然后在 LangBot 中:
|
||||
|
||||
1. 进入"知识库"页面
|
||||
2. 选择一个知识库并进入文档管理
|
||||
3. 上传文件时,在解析器选择器中选择您的插件提供的解析器
|
||||
4. 上传文件后检查文档是否正确摄取
|
||||
@@ -0,0 +1,211 @@
|
||||
---
|
||||
title: "组件:运行器"
|
||||
---
|
||||
|
||||
运行器组件通过 SDK 的 `Runner` 类型实现 Agent 的运行逻辑,也可以按事件注册处理函数,供「插件处理器」使用。两种写法共用 `RunnerContext`,插件安装后由用户选择组件并配置。
|
||||
|
||||
环境准备见[插件开发教程](/zh/plugin/dev/tutor)。
|
||||
|
||||
`RunnerContext` 的完整字段和方法见[上下文 API](/zh/plugin/dev/apis/agent-run#runner)。
|
||||
|
||||
## 添加运行器组件
|
||||
|
||||
单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp Runner`,名称填写 `welcome`,描述填写 `Welcome new members`。
|
||||
|
||||
```bash
|
||||
lbp comp Runner
|
||||
```
|
||||
|
||||
生成的 `components/runner/welcome.yaml` 定义组件信息,`welcome.py` 是处理程序。
|
||||
|
||||
## 自定义运行逻辑
|
||||
|
||||
需要接入模型或实现自己的执行循环时,声明 `spec.usages: [agent]`,在 `run(ctx)` 中返回结果:
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult
|
||||
from langbot_plugin.api.entities.builtin.provider.message import Message
|
||||
|
||||
|
||||
class Echo(Runner):
|
||||
async def run(self, ctx: RunnerContext):
|
||||
yield RunnerResult.run_completed(
|
||||
ctx.run_id,
|
||||
message=Message(role="assistant", content=ctx.input.to_text()),
|
||||
)
|
||||
```
|
||||
|
||||
## 清单文件:运行器
|
||||
|
||||
将 `welcome.yaml` 修改为:
|
||||
|
||||
```yaml
|
||||
apiVersion: langbot/v1
|
||||
kind: Runner
|
||||
metadata:
|
||||
name: welcome
|
||||
label:
|
||||
en_US: Welcome members
|
||||
zh_Hans: 欢迎新成员
|
||||
ja_JP: 新しいメンバーを歓迎
|
||||
description:
|
||||
en_US: Welcome new members and respond to /hello.
|
||||
zh_Hans: 欢迎新成员,并响应 /hello 消息。
|
||||
ja_JP: 新しいメンバーを歓迎し、/hello に返信します。
|
||||
spec:
|
||||
usages: [event]
|
||||
events:
|
||||
- group.member_joined
|
||||
- message.received
|
||||
capabilities:
|
||||
tool_calling: true
|
||||
permissions:
|
||||
tools: [detail, call]
|
||||
config:
|
||||
- name: greeting
|
||||
type: string
|
||||
label:
|
||||
en_US: Greeting
|
||||
zh_Hans: 欢迎语
|
||||
ja_JP: 歓迎メッセージ
|
||||
required: true
|
||||
default: Welcome aboard!
|
||||
- name: reply_enabled
|
||||
type: boolean
|
||||
label:
|
||||
en_US: Send replies
|
||||
zh_Hans: 发送回复
|
||||
ja_JP: 返信する
|
||||
default: true
|
||||
execution:
|
||||
python:
|
||||
path: ./welcome.py
|
||||
attr: Welcome
|
||||
```
|
||||
|
||||
`spec.events` 指定支持的事件,`spec.config` 定义用户可在处理器详情页修改的配置。本例支持成员入群和收到消息,并提供欢迎语、回复开关两个配置项。回复消息需要上面的 `tool_calling` 和 `permissions.tools` 配置。
|
||||
|
||||
|
||||
`spec.usages` 必填、无默认值,决定组件可在哪类处理器中选择:`agent` 用于 Agent 和流水线,`event` 用于插件处理器。可同时声明 `[agent, event]`;包含 `event` 时必须填写 `spec.events`。本例使用事件处理函数,也可以在 `run(ctx)` 中按需调用 `await super().run(ctx)`。
|
||||
|
||||
## 插件处理
|
||||
|
||||
在 `Welcome` 类的 `initialize` 方法中注册事件处理函数。将 `welcome.py` 修改为:
|
||||
|
||||
```python
|
||||
from langbot_plugin.api.definition.components.runner import (
|
||||
Runner,
|
||||
RunnerContext,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.platform.events import (
|
||||
MemberJoinedEvent,
|
||||
MessageReceivedEvent,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.platform.message import Plain
|
||||
|
||||
|
||||
class Welcome(Runner):
|
||||
async def initialize(self):
|
||||
await super().initialize()
|
||||
|
||||
@self.handler(MemberJoinedEvent)
|
||||
async def on_join(ctx: RunnerContext):
|
||||
name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id)
|
||||
await ctx.log(f"Member joined: {name}")
|
||||
if not ctx.config.get("reply_enabled", True):
|
||||
await ctx.log("Replies are disabled for this processor")
|
||||
return
|
||||
greeting = ctx.config.get("greeting", "Welcome aboard!")
|
||||
await ctx.reply(f"{name}, {greeting}")
|
||||
await ctx.log("Welcome action completed")
|
||||
|
||||
@self.handler(MessageReceivedEvent)
|
||||
async def on_message(ctx: RunnerContext):
|
||||
text = "".join(
|
||||
item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain)
|
||||
).strip()
|
||||
if text != "/hello":
|
||||
await ctx.log("Message ignored: expected /hello")
|
||||
return
|
||||
if ctx.config.get("reply_enabled", True):
|
||||
await ctx.reply(ctx.config.get("greeting", "Welcome aboard!"))
|
||||
else:
|
||||
await ctx.log("Replies are disabled for this processor")
|
||||
```
|
||||
|
||||
`on_join` 在成员入群时发送欢迎语,`on_message` 回复 `/hello`。关闭「发送回复」后,只记录日志。
|
||||
|
||||
处理函数中,`ctx.platform_event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。
|
||||
|
||||
## 流式回复
|
||||
|
||||
使用 `ctx.reply_stream()` 更新同一条回复,`update(text)` 接收当前的完整文本:
|
||||
|
||||
```python
|
||||
async with ctx.reply_stream() as reply:
|
||||
await reply.update("正在处理…")
|
||||
await reply.update("处理完成。")
|
||||
```
|
||||
|
||||
正常退出 `async with` 时完成回复。平台不支持流式、未开启流式回复或当前不是消息事件时,会在结束后发送一条完整消息;发生异常时不发送缓存中的未完成文本。调试模式仅模拟发送。
|
||||
|
||||
|
||||
## 事件注册
|
||||
|
||||
通过 `@self.handler(事件类)` 注册处理函数,并将对应事件标识加入清单的 `spec.events`。例如,`MemberJoinedEvent` 对应 `group.member_joined`。
|
||||
|
||||
| 事件 | 清单中的标识 | SDK 事件类 |
|
||||
| --- | --- | --- |
|
||||
| 收到消息 | `message.received` | `MessageReceivedEvent` |
|
||||
| 消息被编辑 | `message.edited` | `MessageEditedEvent` |
|
||||
| 消息被删除 | `message.deleted` | `MessageDeletedEvent` |
|
||||
| 消息表情回应 | `message.reaction` | `MessageReactionEvent` |
|
||||
| 收到反馈 | `feedback.received` | `FeedbackReceivedEvent` |
|
||||
| 成员加入群组 | `group.member_joined` | `MemberJoinedEvent` |
|
||||
| 成员离开群组 | `group.member_left` | `MemberLeftEvent` |
|
||||
| 成员被禁言 | `group.member_banned` | `MemberBannedEvent` |
|
||||
| 群组信息更新 | `group.info_updated` | `GroupInfoUpdatedEvent` |
|
||||
| 收到好友申请 | `friend.request_received` | `FriendRequestReceivedEvent` |
|
||||
| 添加好友 | `friend.added` | `FriendAddedEvent` |
|
||||
| 删除好友 | `friend.removed` | `FriendRemovedEvent` |
|
||||
| 机器人被邀请入群 | `bot.invited_to_group` | `BotInvitedToGroupEvent` |
|
||||
| 机器人被移出群组 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` |
|
||||
| 机器人被禁言 | `bot.muted` | `BotMutedEvent` |
|
||||
| 机器人被解除禁言 | `bot.unmuted` | `BotUnmutedEvent` |
|
||||
| 平台自定义事件 | `platform.specific` | `PlatformSpecificEvent` |
|
||||
|
||||
可用事件及字段见 SDK 的 [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)。各平台支持的事件有所不同。
|
||||
|
||||
## 测试事件处理器
|
||||
|
||||
按[插件开发教程](/zh/plugin/dev/tutor)配置调试连接,在插件目录执行 `lbp run`。然后在 LangBot 中:
|
||||
|
||||
1. 创建「插件处理器」,在详情页选择「欢迎新成员」组件,填写配置并保存。
|
||||
2. 在左侧选择「成员加入群组」,填写成员昵称、ID 和群组 ID,点击「运行测试」。
|
||||
3. 查看回复和日志。也可以选择「收到消息」,输入 `/hello` 测试回复。
|
||||
|
||||
调试中的平台回复使用 Mock,不发送真实消息。修改组件 YAML 后,请重启 `lbp run`。
|
||||
|
||||
安装插件后,在机器人详情页的「插件处理器」中添加该配置并保存。也可以直接在这里选择组件、填写配置,创建并绑定新配置。机器人会自动投递组件声明的事件,无需逐条配置事件路由。
|
||||
|
||||
插件处理器与 Agent/流水线路由独立执行,同一事件可以触发多个插件处理器,某个处理器失败不会阻止其他处理。使用同一配置的机器人会共享设置和运行状态;需要分别设置时,创建不同的配置。可选功能应通过组件配置提供,例如是否回复消息,避免与其他处理器重复回复。
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
一个工作区内每个插件只安装一次,但这份安装会被**多个处理器**(Agent / 流水线)复用。Runner 需要自己保证处理器之间不串数据:
|
||||
|
||||
- `ctx.state.*` 已经按处理器隔离:其作用域包含 `binding_identity`(由 Agent UUID 派生),同一安装下的不同处理器不会互相覆盖。
|
||||
- 平台的插件存储键是 `(实例, 工作区, 组件类型, 插件名, key)`,**不含处理器维度**;所以你自己写进 Host 存储或缓存的键,必须额外包含处理器或会话身份。
|
||||
- `RunnerContext`(`ctx`)只在单次运行内有效,不要把它或它的字段保存到实例上跨调用复用;需要跨调用保留的会话状态用 `ctx.state_get()` / `ctx.state_set()`。
|
||||
- 多个处理器会共享同一个 Runner 实例,不要在实例字段上累积当前运行的数据;如果运行器必须依赖进程内持有的跨调用状态(例如长期驻留的外部会话进程),就不要声明共享运行,保持专属。
|
||||
- 如果按安装维护了进程内缓存,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 接下来做什么
|
||||
|
||||
- 查看 [`RunnerContext` 上下文 API](/zh/plugin/dev/apis/agent-run#runner)。
|
||||
- 查看模型、工具、知识库和存储相关的 [LangBot API](/zh/plugin/dev/apis/common)。
|
||||
- 查看消息发送等[平台 API](/zh/plugin/dev/apis/platform)。
|
||||
- 查看 [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo) 中的更多示例。
|
||||
- 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: "组件:工具"
|
||||
---
|
||||
|
||||
LangBot 内置的 Local Agent 在执行期间会根据场景调用工具以与外界交互,目前支持通过插件和 MCP 两种方式添加工具。
|
||||
|
||||
## 添加工具组件
|
||||
|
||||
单个插件中能添加任意数量的工具,请在插件目录执行命令`lbp comp Tool`,并根据提示输入工具的配置。
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > lbp comp Tool
|
||||
Generating component Tool...
|
||||
Tool name: get_weather_alerts
|
||||
Tool description: Get weather alerts for a US state.
|
||||
Component Tool generated successfully.
|
||||
组件 Tool 生成成功。
|
||||
```
|
||||
|
||||
现在即会在`components/tools/`目录下生成`get_weather_alerts.yaml`和`get_weather_alerts.py`文件,`.yaml`定义了工具的基础信息,`.py`是该工具的处理程序:
|
||||
|
||||
```bash
|
||||
➜ HelloPlugin > tree
|
||||
...
|
||||
├── components
|
||||
│ ├── __init__.py
|
||||
│ └── tools
|
||||
│ ├── __init__.py
|
||||
│ ├── get_weather_alerts.py
|
||||
│ └── get_weather_alerts.yaml
|
||||
...
|
||||
```
|
||||
|
||||
## 清单文件:工具
|
||||
|
||||
```yaml
|
||||
apiVersion: v1 # 请勿修改
|
||||
kind: Tool # 请勿修改
|
||||
metadata:
|
||||
name: get_weather_alerts # 工具名称,供 LLM 识别
|
||||
label:
|
||||
en_US: GetWeatherAlerts # 工具显示名称,用于显示在 LangBot 的 UI 上,支持多语言
|
||||
zh_Hans: GetWeatherAlerts
|
||||
description:
|
||||
en_US: 'Get weather alerts for a US state.' # 工具描述,用于显示在 LangBot 的 UI 上,支持多语言。可选
|
||||
zh_Hans: '获取美国某个州的天气预警'
|
||||
spec:
|
||||
parameters: [] # 工具参数,由 LLM 根据对话上下文生成值
|
||||
llm_prompt: 'Get weather alerts for a US state.' # 工具提示词,供 LLM 判断是否调用该工具
|
||||
execution:
|
||||
python:
|
||||
path: get_weather_alerts.py # 工具处理程序,请勿修改
|
||||
attr: GetWeatherAlerts # 工具处理程序的类名,与 get_weather_alerts.py 中的类名一致
|
||||
```
|
||||
|
||||
## 插件处理
|
||||
|
||||
默认会生成如下代码(`components/tools/<工具名称>.py`),您需要在`GetWeatherAlerts`类的`call`方法中实现此工具的调用和返回逻辑。
|
||||
|
||||
```python
|
||||
# Auto generated by LangBot Plugin SDK.
|
||||
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langbot_plugin.api.definition.components.tool.tool import Tool
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
|
||||
|
||||
class GetWeatherAlerts(Tool):
|
||||
|
||||
async def call(self, params: dict[str, Any], session: provider_session.Session, query_id: int) -> str:
|
||||
"""Fill your tool code here"""
|
||||
return {}
|
||||
```
|
||||
|
||||
在 `call` 方法中,实现该工具:
|
||||
|
||||
<Info>
|
||||
天气获取工具用例来自 MCP 的 [Server 编写示例](https://modelcontextprotocol.io/quickstart/server)。
|
||||
</Info>
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langbot_plugin.api.definition.components.tool.tool import Tool
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
|
||||
import httpx
|
||||
|
||||
# Constants
|
||||
NWS_API_BASE = "https://api.weather.gov"
|
||||
USER_AGENT = "weather-app/1.0"
|
||||
|
||||
|
||||
async def make_nws_request(url: str) -> dict[str, Any] | None:
|
||||
"""Make a request to the NWS API with proper error handling."""
|
||||
headers = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application/geo+json"
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(url, headers=headers, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def format_alert(feature: dict) -> str:
|
||||
"""Format an alert feature into a readable string."""
|
||||
props = feature["properties"]
|
||||
return f"""
|
||||
Event: {props.get('event', 'Unknown')}
|
||||
Area: {props.get('areaDesc', 'Unknown')}
|
||||
Severity: {props.get('severity', 'Unknown')}
|
||||
Description: {props.get('description', 'No description available')}
|
||||
Instructions: {props.get('instruction', 'No specific instructions provided')}
|
||||
"""
|
||||
|
||||
class GetWeatherAlerts(Tool):
|
||||
|
||||
async def call(self, params: dict[str, Any], session: provider_session.Session, query_id: int) -> str:
|
||||
"""Fill your tool code here"""
|
||||
state = params.get("state", "CA")
|
||||
|
||||
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
|
||||
data = await make_nws_request(url)
|
||||
|
||||
if not data or "features" not in data:
|
||||
return "Unable to fetch alerts or no alerts found."
|
||||
|
||||
if not data["features"]:
|
||||
return "No active alerts for this state."
|
||||
|
||||
alerts = [format_alert(feature) for feature in data["features"]]
|
||||
return "\n---\n".join(alerts)
|
||||
```
|
||||
|
||||
在这个工具中,我们要从`params`中获取`state`参数,并调用 NWS API 获取该州的天气预警,故我们需要在清单文件的`parameters`中定义`state`参数。
|
||||
|
||||
`parameters` 的格式遵循 [JSON Schema](https://json-schema.org/docs),受 [OpenAI Function Calling 特性](https://platform.openai.com/docs/guides/function-calling#defining-functions)支持,且其根类型固定为`type: object`,请在`properties`中添加参数,并在`required`中添加必填参数说明。
|
||||
|
||||
```yaml
|
||||
...
|
||||
spec:
|
||||
parameters:
|
||||
type: object # 请勿修改
|
||||
properties: # 请勿修改,在此处添加参数
|
||||
state:
|
||||
type: string
|
||||
description: 'Two-letter US state code (e.g. CA, NY)'
|
||||
required: # 在此添加必填参数说明
|
||||
- state
|
||||
...
|
||||
```
|
||||
|
||||
本工具中还使用了`httpx`库,我们需要在插件目录的`requirements.txt`中添加`httpx`依赖。
|
||||
|
||||
```txt
|
||||
# HelloPlugin/requirements.txt
|
||||
langbot-plugin # 默认已存在
|
||||
httpx
|
||||
```
|
||||
|
||||
现在在插件目录执行命令`lbp run`,启动调试。在 LangBot 中配置支持工具调用的模型,并在对应流水线上选择使用该模型,即可使用该工具。
|
||||
|
||||
<img width="600" src="/images/zh/plugin/dev/components/tool_use_in_chat.png" />
|
||||
|
||||
## 一个安装,多份用法
|
||||
|
||||
一个 Tool 会被**多个处理器与会话**调用(同一事件可以触发多个插件处理器):
|
||||
|
||||
- 配置每次调用读取(`self.get_plugin_config()` 或 `ctx.config`),不要在 `initialize()` 中读取后缓存到实例。
|
||||
- 不要把调用参数、返回结果或会话数据留在实例字段或模块级变量上;会话身份从当前调用的 `session` / `query_id` 取。
|
||||
- 并发调用会共享同一个实例,实例字段会被交织写入;需要跨调用保留的数据放 Host 存储,键包含对应处理器或会话身份。
|
||||
- 如果按安装维护了进程内缓存,请在 `on_installation_revoked(binding)` 中释放。
|
||||
|
||||
以上约束的完整说明见[认证插件与共享运行](/zh/plugin/certified-plugins)。
|
||||
|
||||
## 接下来做什么
|
||||
|
||||
您已经了解了工具的基本用法,接下来可以:
|
||||
|
||||
- 查看 Tool 的 [`session` 和 `query_id` 说明](/zh/plugin/dev/apis/agent-run#tool)
|
||||
- 查看 [LangBot API](/zh/plugin/dev/apis/common)
|
||||
Reference in New Issue
Block a user