diff --git a/src/langbot/libs/wecom_ai_bot_api/api.py b/src/langbot/libs/wecom_ai_bot_api/api.py index a33a5e7a2..785e82f28 100644 --- a/src/langbot/libs/wecom_ai_bot_api/api.py +++ b/src/langbot/libs/wecom_ai_bot_api/api.py @@ -17,6 +17,7 @@ from quart import Quart, request, Response, jsonify from langbot.libs.wecom_ai_bot_api import wecombotevent from langbot.libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt + if TYPE_CHECKING: from langbot.pkg.platform.logger import EventLogger from langbot.pkg.utils import httpclient diff --git a/src/langbot/pkg/agent/runner/event_log_store.py b/src/langbot/pkg/agent/runner/event_log_store.py index eb7277146..0212dc46e 100644 --- a/src/langbot/pkg/agent/runner/event_log_store.py +++ b/src/langbot/pkg/agent/runner/event_log_store.py @@ -1,4 +1,5 @@ """EventLog store for writing and querying event records.""" + from __future__ import annotations import json @@ -44,9 +45,7 @@ class EventLogStore: def __init__(self, engine: AsyncEngine): self.engine = engine - self._session_factory = sessionmaker( - engine, class_=AsyncSession, expire_on_commit=False - ) + self._session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async def append_event( self, @@ -101,7 +100,7 @@ class EventLogStore: # Truncate input summary if too long if input_summary and len(input_summary) > self.MAX_INPUT_SUMMARY_LENGTH: - input_summary = input_summary[:self.MAX_INPUT_SUMMARY_LENGTH - 3] + "..." + input_summary = input_summary[: self.MAX_INPUT_SUMMARY_LENGTH - 3] + '...' async with self._session_factory() as session: event = EventLog( @@ -144,9 +143,7 @@ class EventLogStore: Event record as dict, or None if not found """ async with self._session_factory() as session: - result = await session.execute( - sqlalchemy.select(EventLog).where(EventLog.event_id == event_id) - ) + result = await session.execute(sqlalchemy.select(EventLog).where(EventLog.event_id == event_id)) row = result.scalars().first() if row is None: return None @@ -282,9 +279,7 @@ class EventLogStore: ) -> int: """Delete EventLog rows created before the supplied timestamp.""" async with self._session_factory() as session: - result = await session.execute( - sqlalchemy.delete(EventLog).where(EventLog.created_at < before) - ) + result = await session.execute(sqlalchemy.delete(EventLog).where(EventLog.created_at < before)) await session.commit() return result.rowcount or 0 diff --git a/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py b/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py index 7696dfa4e..c00fb129e 100644 --- a/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py +++ b/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py @@ -71,17 +71,8 @@ class EmbedRouterGroup(group.RouterGroup): ``web_page_bot``, is disabled, or has no Pipeline target for messages. """ bot = await self.ap.platform_mgr.resolve_public_bot(bot_uuid) - pipeline_uuid = ( - bot.get_pipeline_target_for_event_type('message.received') - if bot is not None - else None - ) - if ( - bot is not None - and bot.bot_entity.adapter == 'web_page_bot' - and bot.bot_entity.enable - and pipeline_uuid - ): + pipeline_uuid = bot.get_pipeline_target_for_event_type('message.received') if bot is not None else None + if bot is not None and bot.bot_entity.adapter == 'web_page_bot' and bot.bot_entity.enable and pipeline_uuid: return bot, pipeline_uuid return None, None diff --git a/src/langbot/pkg/api/http/controller/groups/resources/tools.py b/src/langbot/pkg/api/http/controller/groups/resources/tools.py index 2d1cf66a0..357618e2d 100644 --- a/src/langbot/pkg/api/http/controller/groups/resources/tools.py +++ b/src/langbot/pkg/api/http/controller/groups/resources/tools.py @@ -14,9 +14,7 @@ class ToolsRouterGroup(group.RouterGroup): self, request_context: RequestContext, ) -> list[dict] | None: - pipeline_uuid = quart.request.args.get( - 'pipeline_uuid' - ) or quart.request.args.get('pipeline_id') + pipeline_uuid = quart.request.args.get('pipeline_uuid') or quart.request.args.get('pipeline_id') bound_plugins: list[str] | None = None bound_mcp_servers: list[str] | None = None @@ -28,14 +26,9 @@ class ToolsRouterGroup(group.RouterGroup): if pipeline is None: return None - extensions_prefs = normalize_extension_preferences( - pipeline.get('extensions_preferences') - ) + extensions_prefs = normalize_extension_preferences(pipeline.get('extensions_preferences')) if not extensions_prefs['enable_all_plugins']: - bound_plugins = [ - f'{plugin["author"]}/{plugin["name"]}' - for plugin in extensions_prefs['plugins'] - ] + bound_plugins = [f'{plugin["author"]}/{plugin["name"]}' for plugin in extensions_prefs['plugins']] if not extensions_prefs['enable_all_mcp_servers']: bound_mcp_servers = extensions_prefs['mcp_servers'] diff --git a/src/langbot/pkg/entity/persistence/transcript.py b/src/langbot/pkg/entity/persistence/transcript.py index 5d66454e7..f975ab245 100644 --- a/src/langbot/pkg/entity/persistence/transcript.py +++ b/src/langbot/pkg/entity/persistence/transcript.py @@ -1,4 +1,5 @@ """Transcript persistence entity for conversation history projection.""" + from __future__ import annotations import sqlalchemy diff --git a/src/langbot/pkg/persistence/alembic/versions/0017_repair_local_workspace_owner.py b/src/langbot/pkg/persistence/alembic/versions/0017_repair_local_workspace_owner.py index c6d5ed36c..caaf613fc 100644 --- a/src/langbot/pkg/persistence/alembic/versions/0017_repair_local_workspace_owner.py +++ b/src/langbot/pkg/persistence/alembic/versions/0017_repair_local_workspace_owner.py @@ -48,12 +48,16 @@ def upgrade() -> None: sa.column('source', sa.String(32)), sa.column('created_by_account_uuid', sa.String(36)), ) - workspace_uuids = conn.execute( - sa.select(workspaces.c.uuid).where( - workspaces.c.instance_uuid == instance_uuid.strip(), - workspaces.c.source == 'local', + workspace_uuids = ( + conn.execute( + sa.select(workspaces.c.uuid).where( + workspaces.c.instance_uuid == instance_uuid.strip(), + workspaces.c.source == 'local', + ) ) - ).scalars().all() + .scalars() + .all() + ) if not workspace_uuids: return if len(workspace_uuids) > 1: @@ -99,10 +103,7 @@ def upgrade() -> None: sa.column('status', sa.String(32)), ) owner_account_uuid = conn.execute( - sa.select(users.c.uuid) - .where(users.c.status == 'active') - .order_by(users.c.id) - .limit(1) + sa.select(users.c.uuid).where(users.c.status == 'active').order_by(users.c.id).limit(1) ).scalar_one_or_none() if owner_account_uuid is None: return diff --git a/src/langbot/pkg/persistence/alembic/versions/58846a8d7a81_add_event_log_and_transcript_tables.py b/src/langbot/pkg/persistence/alembic/versions/58846a8d7a81_add_event_log_and_transcript_tables.py index 7d7c3746a..f04abd990 100644 --- a/src/langbot/pkg/persistence/alembic/versions/58846a8d7a81_add_event_log_and_transcript_tables.py +++ b/src/langbot/pkg/persistence/alembic/versions/58846a8d7a81_add_event_log_and_transcript_tables.py @@ -4,6 +4,7 @@ Revision ID: 58846a8d7a81 Revises: 0005_add_llm_context_length Create Date: 2026-05-23 15:41:47.030841 """ + from alembic import op import sqlalchemy as sa diff --git a/src/langbot/pkg/persistence/alembic/versions/7b2c1d9e4f30_add_transcript_scope_columns.py b/src/langbot/pkg/persistence/alembic/versions/7b2c1d9e4f30_add_transcript_scope_columns.py index 99da93c0e..0458d8cf5 100644 --- a/src/langbot/pkg/persistence/alembic/versions/7b2c1d9e4f30_add_transcript_scope_columns.py +++ b/src/langbot/pkg/persistence/alembic/versions/7b2c1d9e4f30_add_transcript_scope_columns.py @@ -4,6 +4,7 @@ Revision ID: 7b2c1d9e4f30 Revises: 6dfd3dd7f0c7 Create Date: 2026-06-12 """ + from alembic import op import sqlalchemy as sa diff --git a/src/langbot/pkg/persistence/alembic/versions/8d3a1f2c4b6e_add_agent_run_ledger.py b/src/langbot/pkg/persistence/alembic/versions/8d3a1f2c4b6e_add_agent_run_ledger.py index 88773c1b1..2ef480eba 100644 --- a/src/langbot/pkg/persistence/alembic/versions/8d3a1f2c4b6e_add_agent_run_ledger.py +++ b/src/langbot/pkg/persistence/alembic/versions/8d3a1f2c4b6e_add_agent_run_ledger.py @@ -90,9 +90,7 @@ def upgrade() -> None: ) else: _add_column_if_missing('agent_run', sa.Column('queue_name', sa.String(255), nullable=True)) - _add_column_if_missing( - 'agent_run', sa.Column('priority', sa.Integer(), nullable=False, server_default='0') - ) + _add_column_if_missing('agent_run', sa.Column('priority', sa.Integer(), nullable=False, server_default='0')) _add_column_if_missing('agent_run', sa.Column('requested_runtime_id', sa.String(255), nullable=True)) _add_column_if_missing('agent_run', sa.Column('claimed_by_runtime_id', sa.String(255), nullable=True)) _add_column_if_missing('agent_run', sa.Column('claim_token', sa.String(255), nullable=True)) diff --git a/src/langbot/pkg/pipeline/extension_preferences.py b/src/langbot/pkg/pipeline/extension_preferences.py index 42689b1e4..458700f15 100644 --- a/src/langbot/pkg/pipeline/extension_preferences.py +++ b/src/langbot/pkg/pipeline/extension_preferences.py @@ -50,22 +50,16 @@ def normalize_extension_preferences(value: typing.Any) -> dict[str, typing.Any]: normalized['enable_all_plugins'] = value.get('enable_all_plugins', True) is True normalized['enable_all_mcp_servers'] = value.get('enable_all_mcp_servers', True) is True normalized['enable_all_skills'] = value.get('enable_all_skills', True) is True - normalized['mcp_resource_agent_read_enabled'] = ( - value.get('mcp_resource_agent_read_enabled', True) is True - ) + normalized['mcp_resource_agent_read_enabled'] = value.get('mcp_resource_agent_read_enabled', True) is True plugins = value.get('plugins', []) - plugins_are_valid = isinstance(plugins, list) and all( - _valid_plugin_binding(plugin) for plugin in plugins - ) + plugins_are_valid = isinstance(plugins, list) and all(_valid_plugin_binding(plugin) for plugin in plugins) normalized['plugins'] = list(plugins) if plugins_are_valid else [] if not plugins_are_valid: normalized['enable_all_plugins'] = False mcp_servers = value.get('mcp_servers', []) - mcp_servers_are_valid = isinstance(mcp_servers, list) and all( - _valid_name(server) for server in mcp_servers - ) + mcp_servers_are_valid = isinstance(mcp_servers, list) and all(_valid_name(server) for server in mcp_servers) normalized['mcp_servers'] = list(mcp_servers) if mcp_servers_are_valid else [] if not mcp_servers_are_valid: normalized['enable_all_mcp_servers'] = False @@ -151,6 +145,4 @@ def _validate_list_field( raise ValueError(f"{context} field '{field_label}' must be a list") for index, item in enumerate(items): if not item_validator(item): - raise ValueError( - f"{context} field '{field_label}[{index}]' must be {item_description}" - ) + raise ValueError(f"{context} field '{field_label}[{index}]' must be {item_description}") diff --git a/src/langbot/pkg/platform/adapters/officialaccount/message_converter.py b/src/langbot/pkg/platform/adapters/officialaccount/message_converter.py index 4def1937b..953f40101 100644 --- a/src/langbot/pkg/platform/adapters/officialaccount/message_converter.py +++ b/src/langbot/pkg/platform/adapters/officialaccount/message_converter.py @@ -65,8 +65,12 @@ class OfficialAccountMessageConverter(abstract_platform_adapter.AbstractMessageC else: components.append(platform_message.Unknown(text='[officialaccount voice message without media id]')) elif event.type == 'event': - components.append(platform_message.Unknown(text=f'[officialaccount event: {event.detail_type or "unknown"}]')) + components.append( + platform_message.Unknown(text=f'[officialaccount event: {event.detail_type or "unknown"}]') + ) else: - components.append(platform_message.Unknown(text=f'[unsupported officialaccount msgtype: {event.type or "unknown"}]')) + components.append( + platform_message.Unknown(text=f'[unsupported officialaccount msgtype: {event.type or "unknown"}]') + ) return platform_message.MessageChain(components) diff --git a/src/langbot/pkg/platform/adapters/qqofficial/__init__.py b/src/langbot/pkg/platform/adapters/qqofficial/__init__.py index d87bfc1dc..2d920ed1c 100644 --- a/src/langbot/pkg/platform/adapters/qqofficial/__init__.py +++ b/src/langbot/pkg/platform/adapters/qqofficial/__init__.py @@ -3,4 +3,3 @@ from langbot.pkg.platform.adapters.qqofficial.adapter import QQOfficialAdapter __all__ = ['QQOfficialAdapter'] - diff --git a/src/langbot/pkg/platform/adapters/qqofficial/api_impl.py b/src/langbot/pkg/platform/adapters/qqofficial/api_impl.py index d71847672..59ed22956 100644 --- a/src/langbot/pkg/platform/adapters/qqofficial/api_impl.py +++ b/src/langbot/pkg/platform/adapters/qqofficial/api_impl.py @@ -54,7 +54,9 @@ class QQOfficialAPIMixin: self, group_id: typing.Union[int, str], ) -> list[platform_entities.UserGroupMember]: - return [member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id)] + return [ + member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id) + ] async def edit_message( self, @@ -100,4 +102,3 @@ class QQOfficialAPIMixin: async def leave_group(self, group_id: typing.Union[int, str]): raise NotSupportedError('leave_group') - diff --git a/src/langbot/pkg/platform/adapters/qqofficial/errors.py b/src/langbot/pkg/platform/adapters/qqofficial/errors.py index 72483b096..b56f459ae 100644 --- a/src/langbot/pkg/platform/adapters/qqofficial/errors.py +++ b/src/langbot/pkg/platform/adapters/qqofficial/errors.py @@ -8,4 +8,3 @@ except ModuleNotFoundError: def __init__(self, api_name: str, *args): super().__init__(f"API '{api_name}' is not supported by this adapter", *args) self.api_name = api_name - diff --git a/src/langbot/pkg/platform/adapters/qqofficial/platform_api.py b/src/langbot/pkg/platform/adapters/qqofficial/platform_api.py index b205cc774..143daaae7 100644 --- a/src/langbot/pkg/platform/adapters/qqofficial/platform_api.py +++ b/src/langbot/pkg/platform/adapters/qqofficial/platform_api.py @@ -34,4 +34,3 @@ PLATFORM_API_MAP: dict[str, typing.Callable[[typing.Any, dict], typing.Awaitable 'get_gateway_url': get_gateway_url, 'get_mode': get_mode, } - diff --git a/src/langbot/pkg/platform/adapters/slack/api_impl.py b/src/langbot/pkg/platform/adapters/slack/api_impl.py index 46bc382a9..500738b15 100644 --- a/src/langbot/pkg/platform/adapters/slack/api_impl.py +++ b/src/langbot/pkg/platform/adapters/slack/api_impl.py @@ -47,7 +47,9 @@ class SlackAPIMixin: self, group_id: typing.Union[int, str], ) -> list[platform_entities.UserGroupMember]: - return [member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id)] + return [ + member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id) + ] async def get_group_member_info( self, diff --git a/src/langbot/pkg/platform/adapters/slack/event_converter.py b/src/langbot/pkg/platform/adapters/slack/event_converter.py index 531f4082b..6ebd48d91 100644 --- a/src/langbot/pkg/platform/adapters/slack/event_converter.py +++ b/src/langbot/pkg/platform/adapters/slack/event_converter.py @@ -19,7 +19,9 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter): async def yiri2target(event: platform_events.Event) -> typing.Any: return getattr(event, 'source_platform_object', None) - async def target2legacy(self, event: SlackEvent) -> platform_events.FriendMessage | platform_events.GroupMessage | None: + async def target2legacy( + self, event: SlackEvent + ) -> platform_events.FriendMessage | platform_events.GroupMessage | None: eba_event = await self.target2yiri(event) if not isinstance(eba_event, platform_events.MessageReceivedEvent): return None diff --git a/src/langbot/pkg/platform/adapters/telegram/__init__.py b/src/langbot/pkg/platform/adapters/telegram/__init__.py index f4d2d73d7..b41991e1c 100644 --- a/src/langbot/pkg/platform/adapters/telegram/__init__.py +++ b/src/langbot/pkg/platform/adapters/telegram/__init__.py @@ -1,3 +1,3 @@ from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter -__all__ = ["TelegramAdapter"] +__all__ = ['TelegramAdapter'] diff --git a/src/langbot/pkg/platform/adapters/telegram/platform_api.py b/src/langbot/pkg/platform/adapters/telegram/platform_api.py index ccd81950d..56ca5dd56 100644 --- a/src/langbot/pkg/platform/adapters/telegram/platform_api.py +++ b/src/langbot/pkg/platform/adapters/telegram/platform_api.py @@ -14,7 +14,7 @@ async def pin_message(bot: telegram.Bot, params: dict) -> dict: message_id=params['message_id'], disable_notification=params.get('disable_notification', False), ) - return {"ok": True} + return {'ok': True} async def unpin_message(bot: telegram.Bot, params: dict) -> dict: @@ -23,26 +23,26 @@ async def unpin_message(bot: telegram.Bot, params: dict) -> dict: chat_id=params['chat_id'], message_id=params.get('message_id'), ) - return {"ok": True} + return {'ok': True} async def unpin_all_messages(bot: telegram.Bot, params: dict) -> dict: """Unpin all messages in a chat.""" await bot.unpin_all_chat_messages(chat_id=params['chat_id']) - return {"ok": True} + return {'ok': True} async def get_chat_administrators(bot: telegram.Bot, params: dict) -> dict: """Get chat administrator list.""" admins = await bot.get_chat_administrators(chat_id=params['chat_id']) return { - "administrators": [ + 'administrators': [ { - "user_id": a.user.id, - "username": a.user.username, - "first_name": a.user.first_name, - "status": a.status, - "custom_title": getattr(a, 'custom_title', None), + 'user_id': a.user.id, + 'username': a.user.username, + 'first_name': a.user.first_name, + 'status': a.status, + 'custom_title': getattr(a, 'custom_title', None), } for a in admins ] @@ -55,7 +55,7 @@ async def set_chat_title(bot: telegram.Bot, params: dict) -> dict: chat_id=params['chat_id'], title=params['title'], ) - return {"ok": True} + return {'ok': True} async def set_chat_description(bot: telegram.Bot, params: dict) -> dict: @@ -64,13 +64,13 @@ async def set_chat_description(bot: telegram.Bot, params: dict) -> dict: chat_id=params['chat_id'], description=params.get('description', ''), ) - return {"ok": True} + return {'ok': True} async def get_chat_member_count(bot: telegram.Bot, params: dict) -> dict: """Get chat member count.""" count = await bot.get_chat_member_count(chat_id=params['chat_id']) - return {"count": count} + return {'count': count} async def send_chat_action(bot: telegram.Bot, params: dict) -> dict: @@ -79,7 +79,7 @@ async def send_chat_action(bot: telegram.Bot, params: dict) -> dict: chat_id=params['chat_id'], action=params.get('action', 'typing'), ) - return {"ok": True} + return {'ok': True} async def create_chat_invite_link(bot: telegram.Bot, params: dict) -> dict: @@ -91,10 +91,10 @@ async def create_chat_invite_link(bot: telegram.Bot, params: dict) -> dict: member_limit=params.get('member_limit'), ) return { - "invite_link": link.invite_link, - "name": link.name, - "is_primary": link.is_primary, - "is_revoked": link.is_revoked, + 'invite_link': link.invite_link, + 'name': link.name, + 'is_primary': link.is_primary, + 'is_revoked': link.is_revoked, } @@ -106,20 +106,20 @@ async def answer_callback_query(bot: telegram.Bot, params: dict) -> dict: show_alert=params.get('show_alert', False), url=params.get('url'), ) - return {"ok": True} + return {'ok': True} # ---- Action dispatch table ---- PLATFORM_API_MAP: dict[str, typing.Callable[[telegram.Bot, dict], typing.Awaitable[dict]]] = { - "pin_message": pin_message, - "unpin_message": unpin_message, - "unpin_all_messages": unpin_all_messages, - "get_chat_administrators": get_chat_administrators, - "set_chat_title": set_chat_title, - "set_chat_description": set_chat_description, - "get_chat_member_count": get_chat_member_count, - "send_chat_action": send_chat_action, - "create_chat_invite_link": create_chat_invite_link, - "answer_callback_query": answer_callback_query, + 'pin_message': pin_message, + 'unpin_message': unpin_message, + 'unpin_all_messages': unpin_all_messages, + 'get_chat_administrators': get_chat_administrators, + 'set_chat_title': set_chat_title, + 'set_chat_description': set_chat_description, + 'get_chat_member_count': get_chat_member_count, + 'send_chat_action': send_chat_action, + 'create_chat_invite_link': create_chat_invite_link, + 'answer_callback_query': answer_callback_query, } diff --git a/src/langbot/pkg/platform/adapters/telegram/types.py b/src/langbot/pkg/platform/adapters/telegram/types.py index d36239ff6..eeb04ca15 100644 --- a/src/langbot/pkg/platform/adapters/telegram/types.py +++ b/src/langbot/pkg/platform/adapters/telegram/types.py @@ -7,7 +7,8 @@ from enum import Enum class TelegramChatType(str, Enum): """Telegram chat type.""" - PRIVATE = "private" - GROUP = "group" - SUPERGROUP = "supergroup" - CHANNEL = "channel" + + PRIVATE = 'private' + GROUP = 'group' + SUPERGROUP = 'supergroup' + CHANNEL = 'channel' diff --git a/src/langbot/pkg/platform/adapters/wecombot/api_impl.py b/src/langbot/pkg/platform/adapters/wecombot/api_impl.py index d2255cf85..15758fa5b 100644 --- a/src/langbot/pkg/platform/adapters/wecombot/api_impl.py +++ b/src/langbot/pkg/platform/adapters/wecombot/api_impl.py @@ -54,7 +54,9 @@ class WecomBotAPIMixin: self, group_id: typing.Union[int, str], ) -> list[platform_entities.UserGroupMember]: - return [member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id)] + return [ + member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id) + ] async def upload_file(self, file_data: bytes, filename: str) -> str: raise NotSupportedError('upload_file') diff --git a/src/langbot/pkg/platform/adapters/wecombot/event_converter.py b/src/langbot/pkg/platform/adapters/wecombot/event_converter.py index 5b94ce15e..31fefb559 100644 --- a/src/langbot/pkg/platform/adapters/wecombot/event_converter.py +++ b/src/langbot/pkg/platform/adapters/wecombot/event_converter.py @@ -19,7 +19,9 @@ class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter): async def yiri2target(event: platform_events.Event) -> typing.Any: return getattr(event, 'source_platform_object', None) - async def target2legacy(self, event: WecomBotEvent) -> platform_events.FriendMessage | platform_events.GroupMessage | None: + async def target2legacy( + self, event: WecomBotEvent + ) -> platform_events.FriendMessage | platform_events.GroupMessage | None: eba_event = await self.target2yiri(event) if not isinstance(eba_event, platform_events.MessageReceivedEvent): return None @@ -50,7 +52,9 @@ class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter): async def target2yiri(self, event: WecomBotEvent) -> platform_events.Event: if event.type in {'single', 'group'} and event.msgtype != 'event': return await self.message_to_eba(event) - return self.platform_specific(event, f'wecombot.{event.get("eventtype") or event.msgtype or event.type or "unknown"}') + return self.platform_specific( + event, f'wecombot.{event.get("eventtype") or event.msgtype or event.type or "unknown"}' + ) async def message_to_eba(self, event: WecomBotEvent) -> platform_events.MessageReceivedEvent: sender = platform_entities.User(id=event.userid, nickname=event.username or event.userid) diff --git a/src/langbot/pkg/platform/adapters/wecomcs/event_converter.py b/src/langbot/pkg/platform/adapters/wecomcs/event_converter.py index 7c0743657..3dc56c68e 100644 --- a/src/langbot/pkg/platform/adapters/wecomcs/event_converter.py +++ b/src/langbot/pkg/platform/adapters/wecomcs/event_converter.py @@ -17,7 +17,9 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter): return getattr(event, 'source_platform_object', None) @staticmethod - async def target2legacy(event: WecomCSEvent, bot: WecomCSClient | None = None) -> platform_events.FriendMessage | None: + async def target2legacy( + event: WecomCSEvent, bot: WecomCSClient | None = None + ) -> platform_events.FriendMessage | None: eba_event = await WecomCSEventConverter.target2yiri(event, bot) if hasattr(eba_event, 'to_legacy_event'): return eba_event.to_legacy_event() @@ -30,7 +32,9 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter): return WecomCSEventConverter.platform_specific(event, f'wecomcs.{event.type or "unknown"}') @staticmethod - async def message_to_eba(event: WecomCSEvent, bot: WecomCSClient | None = None) -> platform_events.MessageReceivedEvent: + async def message_to_eba( + event: WecomCSEvent, bot: WecomCSClient | None = None + ) -> platform_events.MessageReceivedEvent: message_chain = await WecomCSMessageConverter.target2yiri(event) sender = await WecomCSEventConverter.user_from_event(event, bot) return platform_events.MessageReceivedEvent( diff --git a/src/langbot/pkg/platform/human_input.py b/src/langbot/pkg/platform/human_input.py index 268885dc0..cc98ba06e 100644 --- a/src/langbot/pkg/platform/human_input.py +++ b/src/langbot/pkg/platform/human_input.py @@ -89,10 +89,12 @@ def format_human_input_text( if actions: lines.append('') if input_defs: - lines.extend([ - 'Reply with action plus field values to continue:', - ' action: ', - ]) + lines.extend( + [ + 'Reply with action plus field values to continue:', + ' action: ', + ] + ) else: lines.append('Reply with the number or title to continue:') for idx, action in enumerate(actions, start=1): diff --git a/src/langbot/pkg/provider/tools/loaders/mcp.py b/src/langbot/pkg/provider/tools/loaders/mcp.py index 2f3b61be7..4fe12d9da 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp.py @@ -2327,15 +2327,9 @@ class MCPLoader(loader.ToolLoader): return items - def _session_by_source_id( - self, context: TenantContext, source_id: str - ) -> RuntimeMCPSession | None: + def _session_by_source_id(self, context: TenantContext, source_id: str) -> RuntimeMCPSession | None: return next( - ( - session - for session in self._sessions_for_context(context) - if session.server_uuid == source_id - ), + (session for session in self._sessions_for_context(context) if session.server_uuid == source_id), None, ) @@ -2397,9 +2391,7 @@ class MCPLoader(loader.ToolLoader): source_id: str | None = None, ) -> typing.Any: """执行工具调用""" - execution_context = await self._assert_execution_active( - _execution_context_from_query(query) - ) + execution_context = await self._assert_execution_active(_execution_context_from_query(query)) if source_id is None and name == MCP_TOOL_LIST_RESOURCES: if getattr(query, 'variables', {}).get('_pipeline_mcp_resource_agent_read_enabled', True) is not True: raise ToolExecutionDeniedError(name, 'MCP resource agent reads are disabled') diff --git a/src/langbot/pkg/provider/tools/toolmgr.py b/src/langbot/pkg/provider/tools/toolmgr.py index 13461e7da..b09dff9d8 100644 --- a/src/langbot/pkg/provider/tools/toolmgr.py +++ b/src/langbot/pkg/provider/tools/toolmgr.py @@ -368,9 +368,7 @@ class ToolManager: return None return await self.plugin_tool_loader.get_tool(name, source_id=source_id) if source == 'mcp': - return await self.mcp_tool_loader.get_tool( - context, name, source_id=source_id - ) + return await self.mcp_tool_loader.get_tool(context, name, source_id=source_id) return None async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list: @@ -488,9 +486,7 @@ class ToolManager: if source_ref is not None: execution_context = get_query_execution_context(query) await self._bind_plugin_workspace(execution_context) - sandbox_available = await self._workspace_sandbox_available( - execution_context - ) + sandbox_available = await self._workspace_sandbox_available(execution_context) source = source_ref['source'] source_id = source_ref.get('source_id') uses_source_id = False diff --git a/tests/integration/persistence/test_migrations.py b/tests/integration/persistence/test_migrations.py index da0ffd89d..f157b4199 100644 --- a/tests/integration/persistence/test_migrations.py +++ b/tests/integration/persistence/test_migrations.py @@ -108,7 +108,6 @@ class TestSQLiteMigrationUpgrade: await run_alembic_upgrade(sqlite_engine, 'head') assert await get_alembic_current(sqlite_engine) == _get_script_head() - assert _get_script_head() == '0023_drop_agent_enabled' @pytest.mark.asyncio async def test_upgrade_from_development_workspace_head_to_merged_head(self, sqlite_engine): @@ -120,7 +119,6 @@ class TestSQLiteMigrationUpgrade: await run_alembic_upgrade(sqlite_engine, 'head') assert await get_alembic_current(sqlite_engine) == _get_script_head() - assert _get_script_head() == '0023_drop_agent_enabled' @pytest.mark.asyncio async def test_upgrade_from_reasoning_config_head_to_merged_head(self, sqlite_engine): @@ -131,7 +129,7 @@ class TestSQLiteMigrationUpgrade: await run_alembic_stamp(sqlite_engine, '0018_llm_reasoning_config') await run_alembic_upgrade(sqlite_engine, 'head') - assert await get_alembic_current(sqlite_engine) == '0023_drop_agent_enabled' + assert await get_alembic_current(sqlite_engine) == _get_script_head() @pytest.mark.asyncio async def test_upgrade_removes_agent_enabled_column(self, sqlite_engine): diff --git a/web/tests/unit/agent-debug-execution.test.mjs b/web/tests/unit/agent-debug-execution.test.mjs index cd39ca6f1..75ab910ad 100644 --- a/web/tests/unit/agent-debug-execution.test.mjs +++ b/web/tests/unit/agent-debug-execution.test.mjs @@ -2,69 +2,137 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import test from 'node:test'; import ts from 'typescript'; -const source = fs.readFileSync(new URL('../../src/app/home/agents/components/debug-execution.ts', import.meta.url), 'utf8'); +const source = fs.readFileSync( + new URL( + '../../src/app/home/agents/components/debug-execution.ts', + import.meta.url, + ), + 'utf8', +); const module = { exports: {} }; -new Function('exports', ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText)(module.exports); +new Function( + 'exports', + ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2022, + }, + }).outputText, +)(module.exports); const { executionSteps } = module.exports; -const event = (type, data) => ({type, data}); +const event = (type, data) => ({ type, data }); test('separates streamed thinking and text, replaces final snapshot without duplication', () => { - assert.deepEqual(executionSteps([ - event('message.delta', {chunk: {content:'plan'}}), - event('message.delta', {chunk: {content:'hello'}}), - event('message.completed', {message: {content:'planhello'}}), - event('run.completed', {message: {content:'hello'}}), - ]), [{kind:'message', text:'hello', reasoning:'plan'}]); + assert.deepEqual( + executionSteps([ + event('message.delta', { chunk: { content: 'plan' } }), + event('message.delta', { chunk: { content: 'hello' } }), + event('message.completed', { + message: { content: 'planhello' }, + }), + event('run.completed', { message: { content: 'hello' } }), + ]), + [{ kind: 'message', text: 'hello', reasoning: 'plan' }], + ); }); test('retains structured reasoning and tool parameters/results in order', () => { const steps = executionSteps([ - event('message.delta', {chunk: {provider_specific_fields:{reasoning_content:'plan'}}}), - event('message.completed', {message: {content:''}}), - event('tool.call.started', {tool_call_id:'1',tool_name:'exec', parameters:{command:'echo hi'}}), - event('tool.call.started', {tool_call_id:'2',tool_name:'exec', parameters:{command:'bad'}}), - event('tool.call.completed', {tool_call_id:'2',tool_name:'exec', error:'failed'}), - event('tool.call.completed', {tool_call_id:'1',tool_name:'exec', result:{stdout:'hi'}}), + event('message.delta', { + chunk: { provider_specific_fields: { reasoning_content: 'plan' } }, + }), + event('message.completed', { message: { content: '' } }), + event('tool.call.started', { + tool_call_id: '1', + tool_name: 'exec', + parameters: { command: 'echo hi' }, + }), + event('tool.call.started', { + tool_call_id: '2', + tool_name: 'exec', + parameters: { command: 'bad' }, + }), + event('tool.call.completed', { + tool_call_id: '2', + tool_name: 'exec', + error: 'failed', + }), + event('tool.call.completed', { + tool_call_id: '1', + tool_name: 'exec', + result: { stdout: 'hi' }, + }), event('run.failed', {}), ]); assert.equal(steps[0].reasoning, 'plan'); assert.equal(steps[1].parameters.command, 'echo hi'); - assert.deepEqual(steps[1].result, {stdout:'hi'}); + assert.deepEqual(steps[1].result, { stdout: 'hi' }); assert.equal(steps[2].status, 'failed'); assert.equal(steps[2].error, 'failed'); }); test('replaces LocalAgent cumulative snapshots instead of repeating text', () => { - assert.deepEqual(executionSteps([ - event('message.delta', {chunk: {content:'hello', msg_sequence:1}}), - event('message.delta', {chunk: {content:'hello world', msg_sequence:2}}), - event('message.delta', {chunk: {content:'hello world', msg_sequence:3, is_final:true}}), - ]), [{kind:'message', text:'hello world', reasoning:''}]); + assert.deepEqual( + executionSteps([ + event('message.delta', { chunk: { content: 'hello', msg_sequence: 1 } }), + event('message.delta', { + chunk: { content: 'hello world', msg_sequence: 2 }, + }), + event('message.delta', { + chunk: { content: 'hello world', msg_sequence: 3, is_final: true }, + }), + ]), + [{ kind: 'message', text: 'hello world', reasoning: '' }], + ); }); test('shows failed tool results even when the call transport completed', () => { const steps = executionSteps([ - event('tool.call.started', {tool_call_id:'exit7', tool_name:'exec', parameters:{command:'exit 7'}}), - event('tool.call.completed', {tool_call_id:'exit7', tool_name:'exec', result:{ok:false, exit_code:7, stderr:'expected'}}), + event('tool.call.started', { + tool_call_id: 'exit7', + tool_name: 'exec', + parameters: { command: 'exit 7' }, + }), + event('tool.call.completed', { + tool_call_id: 'exit7', + tool_name: 'exec', + result: { ok: false, exit_code: 7, stderr: 'expected' }, + }), ]); - assert.equal(steps[0].status,'failed'); - assert.equal(steps[0].result.exit_code,7); + assert.equal(steps[0].status, 'failed'); + assert.equal(steps[0].result.exit_code, 7); }); test('does not repeat prior thinking across LocalAgent tool turns', () => { const prefix = 'first thought'; const steps = executionSteps([ - event('message.delta', {chunk:{content:prefix, msg_sequence:1}}), - event('tool.call.started', {tool_call_id:'w',tool_name:'write',parameters:{path:'/workspace/a'}}), - event('tool.call.completed', {tool_call_id:'w',tool_name:'write',result:{ok:true}}), - event('message.delta', {chunk:{content:prefix+'now read',msg_sequence:1}}), - event('tool.call.started', {tool_call_id:'r',tool_name:'read'}), - event('tool.call.completed', {tool_call_id:'r',tool_name:'read',result:{ok:true}}), - event('message.delta', {chunk:{content:prefix+'now read'+'done',msg_sequence:1}}), - event('message.completed', {message:{content:'done'}}), + event('message.delta', { chunk: { content: prefix, msg_sequence: 1 } }), + event('tool.call.started', { + tool_call_id: 'w', + tool_name: 'write', + parameters: { path: '/workspace/a' }, + }), + event('tool.call.completed', { + tool_call_id: 'w', + tool_name: 'write', + result: { ok: true }, + }), + event('message.delta', { + chunk: { content: prefix + 'now read', msg_sequence: 1 }, + }), + event('tool.call.started', { tool_call_id: 'r', tool_name: 'read' }), + event('tool.call.completed', { + tool_call_id: 'r', + tool_name: 'read', + result: { ok: true }, + }), + event('message.delta', { + chunk: { content: prefix + 'now read' + 'done', msg_sequence: 1 }, + }), + event('message.completed', { message: { content: 'done' } }), ]); - const messages = steps.filter(s=>s.kind==='message'); + const messages = steps.filter((s) => s.kind === 'message'); assert.deepEqual(messages, [ - {kind:'message',text:'',reasoning:'first thought'}, - {kind:'message',text:'now read',reasoning:''}, - {kind:'message',text:'done',reasoning:''}, + { kind: 'message', text: '', reasoning: 'first thought' }, + { kind: 'message', text: 'now read', reasoning: '' }, + { kind: 'message', text: 'done', reasoning: '' }, ]); });