Compare commits

...

16 Commits

Author SHA1 Message Date
Junyan Qin
f1ddddfe00 chore: bump version 4.3.8 2025-10-10 22:50:57 +08:00
Junyan Qin
4e61302156 fix: datetime serialization error in emit_event (#1713) 2025-10-10 22:37:39 +08:00
Junyan Qin
9e3cf418ba perf: output pipeline error in en 2025-10-10 17:55:49 +08:00
Junyan Qin
3e29ec7892 perf: allow not set llm model (#1703) 2025-10-10 16:34:01 +08:00
Junyan Qin
f452742cd2 fix: bad Plain component init in wechatpad (#1712) 2025-10-10 14:48:21 +08:00
Junyan Qin
b560432b0b chore: bump version 4.3.7 2025-10-08 14:36:48 +08:00
Junyan Qin (Chin)
99e5478ced fix: return empty data when plugin system disabled (#1710) 2025-10-07 16:24:38 +08:00
Junyan Qin
09dba91a37 chore: bump version 4.3.7b1 2025-10-07 15:30:33 +08:00
Junyan Qin
18ec4adac9 chore: bump langbot-plugin to 0.1.4b2 2025-10-07 15:25:49 +08:00
Junyan Qin
8bedaa468a chore: add codecov.yml 2025-10-07 00:15:56 +08:00
Guanchao Wang
0ab366fcac Fix/qqo (#1709)
* fix: qq official

* fix: appid
2025-10-07 00:06:07 +08:00
Junyan Qin
d664039e54 feat: update for new events fields 2025-10-06 23:22:38 +08:00
Junyan Qin
6535ba4f72 chore: bump version 4.3.6 2025-10-04 00:22:08 +08:00
Thetail001
3b181cff93 Fix: Correct data type mismatch in AtBotRule (#1705)
Fix can't '@' in QQ group.
2025-10-04 00:20:27 +08:00
Junyan Qin
d1274366a0 chore: release v4.3.5 2025-10-02 10:30:19 +08:00
Junyan Qin
35a4b0f55f chore: bump version v4.3.4 2025-10-02 10:26:48 +08:00
12 changed files with 85 additions and 61 deletions

4
codecov.yml Normal file
View File

@@ -0,0 +1,4 @@
coverage:
status:
project: off
patch: off

View File

@@ -213,7 +213,7 @@ class RuntimePipeline:
await self._execute_from_stage(0, query) await self._execute_from_stage(0, query)
except Exception as e: except Exception as e:
inst_name = query.current_stage_name if query.current_stage_name else 'unknown' inst_name = query.current_stage_name if query.current_stage_name else 'unknown'
self.ap.logger.error(f'处理请求时出错 query_id={query.query_id} stage={inst_name} : {e}') self.ap.logger.error(f'Error processing query {query.query_id} stage={inst_name} : {e}')
self.ap.logger.error(f'Traceback: {traceback.format_exc()}') self.ap.logger.error(f'Traceback: {traceback.format_exc()}')
finally: finally:
self.ap.logger.debug(f'Query {query.query_id} processed') self.ap.logger.debug(f'Query {query.query_id} processed')

View File

@@ -35,11 +35,17 @@ class PreProcessor(stage.PipelineStage):
session = await self.ap.sess_mgr.get_session(query) session = await self.ap.sess_mgr.get_session(query)
# When not local-agent, llm_model is None # When not local-agent, llm_model is None
llm_model = ( try:
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model']) llm_model = (
if selected_runner == 'local-agent' await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
else None if selected_runner == 'local-agent'
) else None
)
except ValueError:
self.ap.logger.warning(
f'LLM model {query.pipeline_config["ai"]["local-agent"]["model"] + " "}not found or not configured'
)
llm_model = None
conversation = await self.ap.sess_mgr.get_conversation( conversation = await self.ap.sess_mgr.get_conversation(
query, query,
@@ -54,7 +60,7 @@ class PreProcessor(stage.PipelineStage):
query.prompt = conversation.prompt.copy() query.prompt = conversation.prompt.copy()
query.messages = conversation.messages.copy() query.messages = conversation.messages.copy()
if selected_runner == 'local-agent': if selected_runner == 'local-agent' and llm_model:
query.use_funcs = [] query.use_funcs = []
query.use_llm_model_uuid = llm_model.model_entity.uuid query.use_llm_model_uuid = llm_model.model_entity.uuid
@@ -72,7 +78,11 @@ class PreProcessor(stage.PipelineStage):
# Check if this model supports vision, if not, remove all images # Check if this model supports vision, if not, remove all images
# TODO this checking should be performed in runner, and in this stage, the image should be reserved # TODO this checking should be performed in runner, and in this stage, the image should be reserved
if selected_runner == 'local-agent' and not llm_model.model_entity.abilities.__contains__('vision'): if (
selected_runner == 'local-agent'
and llm_model
and not llm_model.model_entity.abilities.__contains__('vision')
):
for msg in query.messages: for msg in query.messages:
if isinstance(msg.content, list): if isinstance(msg.content, list):
for me in msg.content: for me in msg.content:
@@ -89,7 +99,9 @@ class PreProcessor(stage.PipelineStage):
content_list.append(provider_message.ContentElement.from_text(me.text)) content_list.append(provider_message.ContentElement.from_text(me.text))
plain_text += me.text plain_text += me.text
elif isinstance(me, platform_message.Image): elif isinstance(me, platform_message.Image):
if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'): if selected_runner != 'local-agent' or (
llm_model and llm_model.model_entity.abilities.__contains__('vision')
):
if me.base64 is not None: if me.base64 is not None:
content_list.append(provider_message.ContentElement.from_image_base64(me.base64)) content_list.append(provider_message.ContentElement.from_image_base64(me.base64))
elif isinstance(me, platform_message.File): elif isinstance(me, platform_message.File):
@@ -100,7 +112,9 @@ class PreProcessor(stage.PipelineStage):
if isinstance(msg, platform_message.Plain): if isinstance(msg, platform_message.Plain):
content_list.append(provider_message.ContentElement.from_text(msg.text)) content_list.append(provider_message.ContentElement.from_text(msg.text))
elif isinstance(msg, platform_message.Image): elif isinstance(msg, platform_message.Image):
if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'): if selected_runner != 'local-agent' or (
llm_model and llm_model.model_entity.abilities.__contains__('vision')
):
if msg.base64 is not None: if msg.base64 is not None:
content_list.append(provider_message.ContentElement.from_image_base64(msg.base64)) content_list.append(provider_message.ContentElement.from_image_base64(msg.base64))

View File

@@ -9,7 +9,6 @@ from .. import handler
from ... import entities from ... import entities
from ....provider import runner as runner_module from ....provider import runner as runner_module
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.events as events import langbot_plugin.api.entities.events as events
from ....utils import importutil from ....utils import importutil
from ....provider import runners from ....provider import runners
@@ -47,18 +46,19 @@ class ChatMessageHandler(handler.MessageHandler):
event_ctx = await self.ap.plugin_connector.emit_event(event) event_ctx = await self.ap.plugin_connector.emit_event(event)
is_create_card = False # 判断下是否需要创建流式卡片 is_create_card = False # 判断下是否需要创建流式卡片
if event_ctx.is_prevented_default(): if event_ctx.is_prevented_default():
if event_ctx.event.reply is not None: if event_ctx.event.reply_message_chain is not None:
mc = platform_message.MessageChain(event_ctx.event.reply) mc = event_ctx.event.reply_message_chain
query.resp_messages.append(mc) query.resp_messages.append(mc)
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
else: else:
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
else: else:
if event_ctx.event.alter is not None: if event_ctx.event.user_message_alter is not None:
# if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter # if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter
query.user_message.content = event_ctx.event.alter query.user_message.content = event_ctx.event.user_message_alter
text_length = 0 text_length = 0
try: try:

View File

@@ -5,7 +5,6 @@ import typing
from .. import handler from .. import handler
from ... import entities from ... import entities
import langbot_plugin.api.entities.builtin.provider.message as provider_message import langbot_plugin.api.entities.builtin.provider.message as provider_message
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.provider.session as provider_session
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
import langbot_plugin.api.entities.events as events import langbot_plugin.api.entities.events as events
@@ -49,8 +48,8 @@ class CommandHandler(handler.MessageHandler):
event_ctx = await self.ap.plugin_connector.emit_event(event) event_ctx = await self.ap.plugin_connector.emit_event(event)
if event_ctx.is_prevented_default(): if event_ctx.is_prevented_default():
if event_ctx.event.reply is not None: if event_ctx.event.reply_message_chain is not None:
mc = platform_message.MessageChain(event_ctx.event.reply) mc = event_ctx.event.reply_message_chain
query.resp_messages.append(mc) query.resp_messages.append(mc)
@@ -59,11 +58,6 @@ class CommandHandler(handler.MessageHandler):
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
else: else:
if event_ctx.event.alter is not None:
query.message_chain = platform_message.MessageChain(
[platform_message.Plain(text=event_ctx.event.alter)]
)
session = await self.ap.sess_mgr.get_session(query) session = await self.ap.sess_mgr.get_session(query)
async for ret in self.ap.cmd_mgr.execute( async for ret in self.ap.cmd_mgr.execute(
@@ -80,8 +74,12 @@ class CommandHandler(handler.MessageHandler):
self.ap.logger.info(f'Command({query.query_id}) error: {self.cut_str(str(ret.error))}') self.ap.logger.info(f'Command({query.query_id}) error: {self.cut_str(str(ret.error))}')
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
elif (ret.text is not None or ret.image_url is not None or ret.image_base64 is not None elif (
or ret.file_url is not None): ret.text is not None
or ret.image_url is not None
or ret.image_base64 is not None
or ret.file_url is not None
):
content: list[provider_message.ContentElement] = [] content: list[provider_message.ContentElement] = []
if ret.text is not None: if ret.text is not None:

View File

@@ -21,7 +21,7 @@ class AtBotRule(rule_model.GroupRespondRule):
def remove_at(message_chain: platform_message.MessageChain): def remove_at(message_chain: platform_message.MessageChain):
nonlocal found nonlocal found
for component in message_chain.root: for component in message_chain.root:
if isinstance(component, platform_message.At) and component.target == query.adapter.bot_account_id: if isinstance(component, platform_message.At) and str(component.target) == str(query.adapter.bot_account_id):
message_chain.remove(component) message_chain.remove(component)
found = True found = True
break break

View File

@@ -80,8 +80,8 @@ class ResponseWrapper(stage.PipelineStage):
new_query=query, new_query=query,
) )
else: else:
if event_ctx.event.reply is not None: if event_ctx.event.reply_message_chain is not None:
query.resp_message_chain.append(platform_message.MessageChain(event_ctx.event.reply)) query.resp_message_chain.append(event_ctx.event.reply_message_chain)
else: else:
query.resp_message_chain.append(result.get_content_platform_message_chain()) query.resp_message_chain.append(result.get_content_platform_message_chain())
@@ -123,10 +123,8 @@ class ResponseWrapper(stage.PipelineStage):
new_query=query, new_query=query,
) )
else: else:
if event_ctx.event.reply is not None: if event_ctx.event.reply_message_chain is not None:
query.resp_message_chain.append( query.resp_message_chain.append(event_ctx.event.reply_message_chain)
platform_message.MessageChain(text=event_ctx.event.reply)
)
else: else:
query.resp_message_chain.append( query.resp_message_chain.append(

View File

@@ -139,19 +139,15 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
event_converter: QQOfficialEventConverter = QQOfficialEventConverter() event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
def __init__(self, config: dict, logger: EventLogger): def __init__(self, config: dict, logger: EventLogger):
self.config = config bot = QQOfficialClient(
self.logger = logger app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger
)
required_keys = [ super().__init__(
'appid', config=config,
'secret', logger=logger,
] bot=bot,
missing_keys = [key for key in required_keys if key not in config] bot_account_id=config['appid'],
if missing_keys:
raise command_errors.ParamNotEnoughError('QQ官方机器人缺少相关配置项请查看文档或联系管理员')
self.bot = QQOfficialClient(
app_id=config['appid'], secret=config['secret'], token=config['token'], logger=self.logger
) )
async def reply_message( async def reply_message(

View File

@@ -139,7 +139,7 @@ class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConvert
pattern = r'@\S{1,20}' pattern = r'@\S{1,20}'
content_no_preifx = re.sub(pattern, '', content_no_preifx) content_no_preifx = re.sub(pattern, '', content_no_preifx)
return platform_message.MessageChain([platform_message.Plain(content_no_preifx)]) return platform_message.MessageChain([platform_message.Plain(text=content_no_preifx)])
async def _handler_image(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: async def _handler_image(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain:
"""处理图像消息 (msg_type=3)""" """处理图像消息 (msg_type=3)"""
@@ -265,7 +265,7 @@ class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConvert
# 文本消息 # 文本消息
try: try:
if '<msg>' not in quote_data: if '<msg>' not in quote_data:
quote_data_message_list.append(platform_message.Plain(quote_data)) quote_data_message_list.append(platform_message.Plain(text=quote_data))
else: else:
# 引用消息展开 # 引用消息展开
quote_data_xml = ET.fromstring(quote_data) quote_data_xml = ET.fromstring(quote_data)
@@ -280,7 +280,7 @@ class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConvert
quote_data_message_list.extend(await self._handler_compound(None, quote_data)) quote_data_message_list.extend(await self._handler_compound(None, quote_data))
except Exception as e: except Exception as e:
self.logger.error(f'处理引用消息异常 expcetion:{e}') self.logger.error(f'处理引用消息异常 expcetion:{e}')
quote_data_message_list.append(platform_message.Plain(quote_data)) quote_data_message_list.append(platform_message.Plain(text=quote_data))
message_list.append( message_list.append(
platform_message.Quote( platform_message.Quote(
sender_id=sender_id, sender_id=sender_id,
@@ -290,7 +290,7 @@ class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConvert
if len(user_data) > 0: if len(user_data) > 0:
pattern = r'@\S{1,20}' pattern = r'@\S{1,20}'
user_data = re.sub(pattern, '', user_data) user_data = re.sub(pattern, '', user_data)
message_list.append(platform_message.Plain(user_data)) message_list.append(platform_message.Plain(text=user_data))
return platform_message.MessageChain(message_list) return platform_message.MessageChain(message_list)
@@ -543,7 +543,6 @@ class WeChatPadAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
] = {} ] = {}
def __init__(self, config: dict, logger: EventLogger): def __init__(self, config: dict, logger: EventLogger):
quart_app = quart.Quart(__name__) quart_app = quart.Quart(__name__)
message_converter = WeChatPadMessageConverter(config, logger) message_converter = WeChatPadMessageConverter(config, logger)
@@ -551,15 +550,14 @@ class WeChatPadAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
bot = WeChatPadClient(config['wechatpad_url'], config['token']) bot = WeChatPadClient(config['wechatpad_url'], config['token'])
super().__init__( super().__init__(
config=config, config=config,
logger = logger, logger=logger,
quart_app = quart_app, quart_app=quart_app,
message_converter =message_converter, message_converter=message_converter,
event_converter = event_converter, event_converter=event_converter,
listeners={}, listeners={},
bot_account_id ='', bot_account_id='',
name="WeChatPad", name='WeChatPad',
bot=bot, bot=bot,
) )
async def ws_message(self, data): async def ws_message(self, data):

View File

@@ -18,7 +18,7 @@ from langbot_plugin.api.entities import events
from langbot_plugin.api.entities import context from langbot_plugin.api.entities import context
import langbot_plugin.runtime.io.connection as base_connection import langbot_plugin.runtime.io.connection as base_connection
from langbot_plugin.api.definition.components.manifest import ComponentManifest from langbot_plugin.api.definition.components.manifest import ComponentManifest
from langbot_plugin.api.entities.builtin.command import context as command_context from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
from ..core import taskmgr from ..core import taskmgr
@@ -191,6 +191,9 @@ class PluginRuntimeConnector:
task_context.trace(trace) task_context.trace(trace)
async def list_plugins(self) -> list[dict[str, Any]]: async def list_plugins(self) -> list[dict[str, Any]]:
if not self.is_enable_plugin:
return []
return await self.handler.list_plugins() return await self.handler.list_plugins()
async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]: async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]:
@@ -211,21 +214,31 @@ class PluginRuntimeConnector:
if not self.is_enable_plugin: if not self.is_enable_plugin:
return event_ctx return event_ctx
event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=True))
event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=False))
event_ctx = context.EventContext.model_validate(event_ctx_result['event_context']) event_ctx = context.EventContext.model_validate(event_ctx_result['event_context'])
return event_ctx return event_ctx
async def list_tools(self) -> list[ComponentManifest]: async def list_tools(self) -> list[ComponentManifest]:
if not self.is_enable_plugin:
return []
list_tools_data = await self.handler.list_tools() list_tools_data = await self.handler.list_tools()
return [ComponentManifest.model_validate(tool) for tool in list_tools_data] return [ComponentManifest.model_validate(tool) for tool in list_tools_data]
async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]: async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]:
if not self.is_enable_plugin:
return {'error': 'Tool not found: plugin system is disabled'}
return await self.handler.call_tool(tool_name, parameters) return await self.handler.call_tool(tool_name, parameters)
async def list_commands(self) -> list[ComponentManifest]: async def list_commands(self) -> list[ComponentManifest]:
if not self.is_enable_plugin:
return []
list_commands_data = await self.handler.list_commands() list_commands_data = await self.handler.list_commands()
return [ComponentManifest.model_validate(command) for command in list_commands_data] return [ComponentManifest.model_validate(command) for command in list_commands_data]
@@ -233,6 +246,9 @@ class PluginRuntimeConnector:
async def execute_command( async def execute_command(
self, command_ctx: command_context.ExecuteContext self, command_ctx: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]: ) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if not self.is_enable_plugin:
yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(command_ctx.command))
gen = self.handler.execute_command(command_ctx.model_dump(serialize_as_any=True)) gen = self.handler.execute_command(command_ctx.model_dump(serialize_as_any=True))
async for ret in gen: async for ret in gen:

View File

@@ -1,4 +1,4 @@
semantic_version = 'v4.3.3' semantic_version = 'v4.3.8'
required_database_version = 8 required_database_version = 8
"""Tag the version of the database schema, used to check if the database needs to be migrated""" """Tag the version of the database schema, used to check if the database needs to be migrated"""

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "langbot" name = "langbot"
version = "4.3.3" version = "4.3.8"
description = "Easy-to-use global IM bot platform designed for LLM era" description = "Easy-to-use global IM bot platform designed for LLM era"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10.1,<4.0" requires-python = ">=3.10.1,<4.0"
@@ -62,7 +62,7 @@ dependencies = [
"langchain>=0.2.0", "langchain>=0.2.0",
"chromadb>=0.4.24", "chromadb>=0.4.24",
"qdrant-client (>=1.15.1,<2.0.0)", "qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.1.3", "langbot-plugin==0.1.4",
"asyncpg>=0.30.0", "asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0", "line-bot-sdk>=3.19.0",
"tboxsdk>=0.0.10", "tboxsdk>=0.0.10",