mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: supported Tbox runner (#1680)
* add tboxsdk * add tbox runner * fix comment & add document link * Update pkg/provider/runners/tboxapi.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: haoxuan.yhx <haoxuan.yhx@antgroup.com> Co-authored-by: haoxuan <haoxuan@U-X69D6XTD-2229.local> Co-authored-by: Junyan Qin (Chin) <rockchinq@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -124,6 +124,7 @@ docker compose up -d
|
|||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
|
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
|
||||||
|
| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台,每月免费10亿大模型Token |
|
||||||
|
|
||||||
### TTS
|
### TTS
|
||||||
|
|
||||||
|
|||||||
205
pkg/provider/runners/tboxapi.py
Normal file
205
pkg/provider/runners/tboxapi.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
from tboxsdk.tbox import TboxClient
|
||||||
|
from tboxsdk.model.file import File, FileType
|
||||||
|
|
||||||
|
from .. import runner
|
||||||
|
from ...core import app
|
||||||
|
from ...utils import image
|
||||||
|
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||||
|
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
||||||
|
|
||||||
|
|
||||||
|
class TboxAPIError(Exception):
|
||||||
|
"""TBox API 请求失败"""
|
||||||
|
|
||||||
|
def __init__(self, message: str):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
@runner.runner_class('tbox-app-api')
|
||||||
|
class TboxAPIRunner(runner.RequestRunner):
|
||||||
|
"蚂蚁百宝箱API对话请求器"
|
||||||
|
|
||||||
|
# 运行器内部使用的配置
|
||||||
|
app_id: str # 蚂蚁百宝箱平台中的应用ID
|
||||||
|
api_key: str # 在蚂蚁百宝箱平台中申请的令牌
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application, pipeline_config: dict):
|
||||||
|
"""初始化"""
|
||||||
|
self.ap = ap
|
||||||
|
self.pipeline_config = pipeline_config
|
||||||
|
|
||||||
|
# 初始化Tbox 参数配置
|
||||||
|
self.app_id = self.pipeline_config['ai']['tbox-app-api']['app-id']
|
||||||
|
self.api_key = self.pipeline_config['ai']['tbox-app-api']['api-key']
|
||||||
|
|
||||||
|
# 初始化Tbox client
|
||||||
|
self.tbox_client = TboxClient(authorization=self.api_key)
|
||||||
|
|
||||||
|
async def _preprocess_user_message(self, query: pipeline_query.Query) -> tuple[str, list[str]]:
|
||||||
|
"""预处理用户消息,提取纯文本,并将图片上传到 Tbox 服务
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, list[str]]: 纯文本和图片的 Tbox 文件ID
|
||||||
|
"""
|
||||||
|
plain_text = ''
|
||||||
|
image_ids = []
|
||||||
|
|
||||||
|
if isinstance(query.user_message.content, list):
|
||||||
|
for ce in query.user_message.content:
|
||||||
|
if ce.type == 'text':
|
||||||
|
plain_text += ce.text
|
||||||
|
elif ce.type == 'image_base64':
|
||||||
|
image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
|
||||||
|
# 创建临时文件
|
||||||
|
file_bytes = base64.b64decode(image_b64)
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=f'.{image_format}', delete=False) as tmp_file:
|
||||||
|
tmp_file.write(file_bytes)
|
||||||
|
tmp_file_path = tmp_file.name
|
||||||
|
file_upload_resp = self.tbox_client.upload_file(
|
||||||
|
tmp_file_path
|
||||||
|
)
|
||||||
|
image_id = file_upload_resp.get("data", "")
|
||||||
|
image_ids.append(image_id)
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if os.path.exists(tmp_file_path):
|
||||||
|
os.unlink(tmp_file_path)
|
||||||
|
elif isinstance(query.user_message.content, str):
|
||||||
|
plain_text = query.user_message.content
|
||||||
|
|
||||||
|
return plain_text, image_ids
|
||||||
|
|
||||||
|
async def _agent_messages(
|
||||||
|
self, query: pipeline_query.Query
|
||||||
|
) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||||
|
"""TBox 智能体对话请求"""
|
||||||
|
|
||||||
|
plain_text, image_ids = await self._preprocess_user_message(query)
|
||||||
|
remove_think = self.pipeline_config['output'].get('misc', {}).get('remove-think')
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_stream = await query.adapter.is_stream_output_supported()
|
||||||
|
except AttributeError:
|
||||||
|
is_stream = False
|
||||||
|
|
||||||
|
# 获取Tbox的conversation_id
|
||||||
|
conversation_id = query.session.using_conversation.uuid or None
|
||||||
|
|
||||||
|
files = None
|
||||||
|
if image_ids:
|
||||||
|
files = [
|
||||||
|
File(file_id=image_id, type=FileType.IMAGE)
|
||||||
|
for image_id in image_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
# 发送对话请求
|
||||||
|
response = self.tbox_client.chat(
|
||||||
|
app_id=self.app_id, # Tbox中智能体应用的ID
|
||||||
|
user_id=query.bot_uuid, # 用户ID
|
||||||
|
query=plain_text, # 用户输入的文本信息
|
||||||
|
stream=is_stream, # 是否流式输出
|
||||||
|
conversation_id=conversation_id, # 会话ID,为None时Tbox会自动创建一个新会话
|
||||||
|
files=files, # 图片内容
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_stream:
|
||||||
|
# 解析Tbox流式输出内容,并发送给上游
|
||||||
|
for chunk in self._process_stream_message(response, query, remove_think):
|
||||||
|
yield chunk
|
||||||
|
else:
|
||||||
|
message = self._process_non_stream_message(response, query, remove_think)
|
||||||
|
yield provider_message.Message(
|
||||||
|
role='assistant',
|
||||||
|
content=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_non_stream_message(self, response: typing.Dict, query: pipeline_query.Query, remove_think: bool):
|
||||||
|
if response.get('errorCode') != "0":
|
||||||
|
raise TboxAPIError(f'Tbox API 请求失败: {response.get("errorMsg", "")}')
|
||||||
|
payload = response.get('data', {})
|
||||||
|
conversation_id = payload.get('conversationId', '')
|
||||||
|
query.session.using_conversation.uuid = conversation_id
|
||||||
|
thinking_content = payload.get('reasoningContent', [])
|
||||||
|
result = ""
|
||||||
|
if thinking_content and not remove_think:
|
||||||
|
result += f'<think>\n{thinking_content[0].get("text", "")}\n</think>\n'
|
||||||
|
content = payload.get('result', [])
|
||||||
|
if content:
|
||||||
|
result += content[0].get('chunk', '')
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _process_stream_message(self, response: typing.Generator[dict], query: pipeline_query.Query, remove_think: bool):
|
||||||
|
idx_msg = 0
|
||||||
|
pending_content = ''
|
||||||
|
conversation_id = None
|
||||||
|
think_start = False
|
||||||
|
think_end = False
|
||||||
|
for chunk in response:
|
||||||
|
if chunk.get('type', '') == 'chunk':
|
||||||
|
"""
|
||||||
|
Tbox返回的消息内容chunk结构
|
||||||
|
{'lane': 'default', 'payload': {'conversationId': '20250918tBI947065406', 'messageId': '20250918TB1f53230954', 'text': '️'}, 'type': 'chunk'}
|
||||||
|
"""
|
||||||
|
# 如果包含思考过程,拼接</think>
|
||||||
|
if think_start and not think_end:
|
||||||
|
pending_content += '\n</think>\n'
|
||||||
|
think_end = True
|
||||||
|
|
||||||
|
payload = chunk.get('payload', {})
|
||||||
|
if not conversation_id:
|
||||||
|
conversation_id = payload.get('conversationId')
|
||||||
|
query.session.using_conversation.uuid = conversation_id
|
||||||
|
if payload.get('text'):
|
||||||
|
idx_msg += 1
|
||||||
|
pending_content += payload.get('text')
|
||||||
|
elif chunk.get('type', '') == 'thinking' and not remove_think:
|
||||||
|
"""
|
||||||
|
Tbox返回的思考过程chunk结构
|
||||||
|
{'payload': '{"ext_data":{"text":"日期"},"event":"flow.node.llm.thinking","entity":{"node_type":"text-completion","execute_id":"6","group_id":0,"parent_execute_id":"6","node_name":"模型推理","node_id":"TC_5u6gl0"}}', 'type': 'thinking'}
|
||||||
|
"""
|
||||||
|
payload = json.loads(chunk.get('payload', '{}'))
|
||||||
|
if payload.get('ext_data', {}).get('text'):
|
||||||
|
idx_msg += 1
|
||||||
|
content = payload.get('ext_data', {}).get('text')
|
||||||
|
if not think_start:
|
||||||
|
think_start = True
|
||||||
|
pending_content += f'<think>\n{content}'
|
||||||
|
else:
|
||||||
|
pending_content += content
|
||||||
|
elif chunk.get('type', '') == 'error':
|
||||||
|
raise TboxAPIError(
|
||||||
|
f'Tbox API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} '
|
||||||
|
)
|
||||||
|
|
||||||
|
if idx_msg % 8 == 0:
|
||||||
|
yield provider_message.MessageChunk(
|
||||||
|
role='assistant',
|
||||||
|
content=pending_content,
|
||||||
|
is_final=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tbox不返回END事件,默认发一个最终消息
|
||||||
|
yield provider_message.MessageChunk(
|
||||||
|
role='assistant',
|
||||||
|
content=pending_content,
|
||||||
|
is_final=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||||
|
"""运行"""
|
||||||
|
msg_seq = 0
|
||||||
|
async for msg in self._agent_messages(query):
|
||||||
|
if isinstance(msg, provider_message.MessageChunk):
|
||||||
|
msg_seq += 1
|
||||||
|
msg.msg_sequence = msg_seq
|
||||||
|
yield msg
|
||||||
@@ -64,7 +64,8 @@ dependencies = [
|
|||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"langbot-plugin==0.1.3b1",
|
"langbot-plugin==0.1.3b1",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0"
|
"line-bot-sdk>=3.19.0",
|
||||||
|
"tboxsdk>=0.0.10",
|
||||||
]
|
]
|
||||||
keywords = [
|
keywords = [
|
||||||
"bot",
|
"bot",
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ stages:
|
|||||||
label:
|
label:
|
||||||
en_US: Local Agent
|
en_US: Local Agent
|
||||||
zh_Hans: 内置 Agent
|
zh_Hans: 内置 Agent
|
||||||
|
- name: tbox-app-api
|
||||||
|
label:
|
||||||
|
en_US: Tbox App API
|
||||||
|
zh_Hans: 蚂蚁百宝箱平台 API
|
||||||
- name: dify-service-api
|
- name: dify-service-api
|
||||||
label:
|
label:
|
||||||
en_US: Dify Service API
|
en_US: Dify Service API
|
||||||
@@ -82,6 +86,26 @@ stages:
|
|||||||
type: knowledge-base-selector
|
type: knowledge-base-selector
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
- name: tbox-app-api
|
||||||
|
label:
|
||||||
|
en_US: Tbox App API
|
||||||
|
zh_Hans: 蚂蚁百宝箱平台 API
|
||||||
|
description:
|
||||||
|
en_US: Configure the Tbox App API of the pipeline
|
||||||
|
zh_Hans: 配置蚂蚁百宝箱平台 API
|
||||||
|
config:
|
||||||
|
- name: api-key
|
||||||
|
label:
|
||||||
|
en_US: API Key
|
||||||
|
zh_Hans: API 密钥
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
- name: app-id
|
||||||
|
label:
|
||||||
|
en_US: App ID
|
||||||
|
zh_Hans: 应用 ID
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
- name: dify-service-api
|
- name: dify-service-api
|
||||||
label:
|
label:
|
||||||
en_US: Dify Service API
|
en_US: Dify Service API
|
||||||
|
|||||||
Reference in New Issue
Block a user