From ed869f7e815a6c7e624b5fb90cd8c257e48699b0 Mon Sep 17 00:00:00 2001 From: yhaoxuan <114649838+yhaoxuan@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:37:15 +0800 Subject: [PATCH] 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 Co-authored-by: haoxuan Co-authored-by: Junyan Qin (Chin) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 1 + pkg/provider/runners/tboxapi.py | 205 ++++++++++++++++++++++++++++ pyproject.toml | 3 +- templates/metadata/pipeline/ai.yaml | 24 ++++ 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 pkg/provider/runners/tboxapi.py diff --git a/README.md b/README.md index 4973023b..13f327fc 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ docker compose up -d | [火山方舟](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) | ✅ | 大模型聚合平台 | | [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 | +| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台,每月免费10亿大模型Token | ### TTS diff --git a/pkg/provider/runners/tboxapi.py b/pkg/provider/runners/tboxapi.py new file mode 100644 index 00000000..f0b1bd6a --- /dev/null +++ b/pkg/provider/runners/tboxapi.py @@ -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'\n{thinking_content[0].get("text", "")}\n\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'} + """ + # 如果包含思考过程,拼接 + if think_start and not think_end: + pending_content += '\n\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'\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 diff --git a/pyproject.toml b/pyproject.toml index 28f50b05..564211c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,8 @@ dependencies = [ "qdrant-client (>=1.15.1,<2.0.0)", "langbot-plugin==0.1.3b1", "asyncpg>=0.30.0", - "line-bot-sdk>=3.19.0" + "line-bot-sdk>=3.19.0", + "tboxsdk>=0.0.10", ] keywords = [ "bot", diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index b37c753b..2b69806c 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -23,6 +23,10 @@ stages: label: en_US: Local Agent zh_Hans: 内置 Agent + - name: tbox-app-api + label: + en_US: Tbox App API + zh_Hans: 蚂蚁百宝箱平台 API - name: dify-service-api label: en_US: Dify Service API @@ -82,6 +86,26 @@ stages: type: knowledge-base-selector required: false 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 label: en_US: Dify Service API