fix(ci): format sources and resolve current migration head

This commit is contained in:
dadachann
2026-09-11 04:32:31 +00:00
parent 6189b06dfc
commit ce3f10a3ea
28 changed files with 210 additions and 163 deletions
+1
View File
@@ -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 import wecombotevent
from langbot.libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt from langbot.libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt
if TYPE_CHECKING: if TYPE_CHECKING:
from langbot.pkg.platform.logger import EventLogger from langbot.pkg.platform.logger import EventLogger
from langbot.pkg.utils import httpclient from langbot.pkg.utils import httpclient
@@ -1,4 +1,5 @@
"""EventLog store for writing and querying event records.""" """EventLog store for writing and querying event records."""
from __future__ import annotations from __future__ import annotations
import json import json
@@ -44,9 +45,7 @@ class EventLogStore:
def __init__(self, engine: AsyncEngine): def __init__(self, engine: AsyncEngine):
self.engine = engine self.engine = engine
self._session_factory = sessionmaker( self._session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
engine, class_=AsyncSession, expire_on_commit=False
)
async def append_event( async def append_event(
self, self,
@@ -101,7 +100,7 @@ class EventLogStore:
# Truncate input summary if too long # Truncate input summary if too long
if input_summary and len(input_summary) > self.MAX_INPUT_SUMMARY_LENGTH: 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: async with self._session_factory() as session:
event = EventLog( event = EventLog(
@@ -144,9 +143,7 @@ class EventLogStore:
Event record as dict, or None if not found Event record as dict, or None if not found
""" """
async with self._session_factory() as session: async with self._session_factory() as session:
result = await session.execute( result = await session.execute(sqlalchemy.select(EventLog).where(EventLog.event_id == event_id))
sqlalchemy.select(EventLog).where(EventLog.event_id == event_id)
)
row = result.scalars().first() row = result.scalars().first()
if row is None: if row is None:
return None return None
@@ -282,9 +279,7 @@ class EventLogStore:
) -> int: ) -> int:
"""Delete EventLog rows created before the supplied timestamp.""" """Delete EventLog rows created before the supplied timestamp."""
async with self._session_factory() as session: async with self._session_factory() as session:
result = await session.execute( result = await session.execute(sqlalchemy.delete(EventLog).where(EventLog.created_at < before))
sqlalchemy.delete(EventLog).where(EventLog.created_at < before)
)
await session.commit() await session.commit()
return result.rowcount or 0 return result.rowcount or 0
@@ -71,17 +71,8 @@ class EmbedRouterGroup(group.RouterGroup):
``web_page_bot``, is disabled, or has no Pipeline target for messages. ``web_page_bot``, is disabled, or has no Pipeline target for messages.
""" """
bot = await self.ap.platform_mgr.resolve_public_bot(bot_uuid) bot = await self.ap.platform_mgr.resolve_public_bot(bot_uuid)
pipeline_uuid = ( pipeline_uuid = bot.get_pipeline_target_for_event_type('message.received') if bot is not None else None
bot.get_pipeline_target_for_event_type('message.received') if bot is not None and bot.bot_entity.adapter == 'web_page_bot' and bot.bot_entity.enable and pipeline_uuid:
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 bot, pipeline_uuid
return None, None return None, None
@@ -14,9 +14,7 @@ class ToolsRouterGroup(group.RouterGroup):
self, self,
request_context: RequestContext, request_context: RequestContext,
) -> list[dict] | None: ) -> list[dict] | None:
pipeline_uuid = quart.request.args.get( pipeline_uuid = quart.request.args.get('pipeline_uuid') or quart.request.args.get('pipeline_id')
'pipeline_uuid'
) or quart.request.args.get('pipeline_id')
bound_plugins: list[str] | None = None bound_plugins: list[str] | None = None
bound_mcp_servers: list[str] | None = None bound_mcp_servers: list[str] | None = None
@@ -28,14 +26,9 @@ class ToolsRouterGroup(group.RouterGroup):
if pipeline is None: if pipeline is None:
return None return None
extensions_prefs = normalize_extension_preferences( extensions_prefs = normalize_extension_preferences(pipeline.get('extensions_preferences'))
pipeline.get('extensions_preferences')
)
if not extensions_prefs['enable_all_plugins']: if not extensions_prefs['enable_all_plugins']:
bound_plugins = [ bound_plugins = [f'{plugin["author"]}/{plugin["name"]}' for plugin in extensions_prefs['plugins']]
f'{plugin["author"]}/{plugin["name"]}'
for plugin in extensions_prefs['plugins']
]
if not extensions_prefs['enable_all_mcp_servers']: if not extensions_prefs['enable_all_mcp_servers']:
bound_mcp_servers = extensions_prefs['mcp_servers'] bound_mcp_servers = extensions_prefs['mcp_servers']
@@ -1,4 +1,5 @@
"""Transcript persistence entity for conversation history projection.""" """Transcript persistence entity for conversation history projection."""
from __future__ import annotations from __future__ import annotations
import sqlalchemy import sqlalchemy
@@ -48,12 +48,16 @@ def upgrade() -> None:
sa.column('source', sa.String(32)), sa.column('source', sa.String(32)),
sa.column('created_by_account_uuid', sa.String(36)), sa.column('created_by_account_uuid', sa.String(36)),
) )
workspace_uuids = conn.execute( workspace_uuids = (
sa.select(workspaces.c.uuid).where( conn.execute(
workspaces.c.instance_uuid == instance_uuid.strip(), sa.select(workspaces.c.uuid).where(
workspaces.c.source == 'local', workspaces.c.instance_uuid == instance_uuid.strip(),
workspaces.c.source == 'local',
)
) )
).scalars().all() .scalars()
.all()
)
if not workspace_uuids: if not workspace_uuids:
return return
if len(workspace_uuids) > 1: if len(workspace_uuids) > 1:
@@ -99,10 +103,7 @@ def upgrade() -> None:
sa.column('status', sa.String(32)), sa.column('status', sa.String(32)),
) )
owner_account_uuid = conn.execute( owner_account_uuid = conn.execute(
sa.select(users.c.uuid) sa.select(users.c.uuid).where(users.c.status == 'active').order_by(users.c.id).limit(1)
.where(users.c.status == 'active')
.order_by(users.c.id)
.limit(1)
).scalar_one_or_none() ).scalar_one_or_none()
if owner_account_uuid is None: if owner_account_uuid is None:
return return
@@ -4,6 +4,7 @@ Revision ID: 58846a8d7a81
Revises: 0005_add_llm_context_length Revises: 0005_add_llm_context_length
Create Date: 2026-05-23 15:41:47.030841 Create Date: 2026-05-23 15:41:47.030841
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
@@ -4,6 +4,7 @@ Revision ID: 7b2c1d9e4f30
Revises: 6dfd3dd7f0c7 Revises: 6dfd3dd7f0c7
Create Date: 2026-06-12 Create Date: 2026-06-12
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
@@ -90,9 +90,7 @@ def upgrade() -> None:
) )
else: else:
_add_column_if_missing('agent_run', sa.Column('queue_name', sa.String(255), nullable=True)) _add_column_if_missing('agent_run', sa.Column('queue_name', sa.String(255), nullable=True))
_add_column_if_missing( _add_column_if_missing('agent_run', sa.Column('priority', sa.Integer(), nullable=False, server_default='0'))
'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('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('claimed_by_runtime_id', sa.String(255), nullable=True))
_add_column_if_missing('agent_run', sa.Column('claim_token', sa.String(255), nullable=True)) _add_column_if_missing('agent_run', sa.Column('claim_token', sa.String(255), nullable=True))
@@ -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_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_mcp_servers'] = value.get('enable_all_mcp_servers', True) is True
normalized['enable_all_skills'] = value.get('enable_all_skills', True) is True normalized['enable_all_skills'] = value.get('enable_all_skills', True) is True
normalized['mcp_resource_agent_read_enabled'] = ( normalized['mcp_resource_agent_read_enabled'] = value.get('mcp_resource_agent_read_enabled', True) is True
value.get('mcp_resource_agent_read_enabled', True) is True
)
plugins = value.get('plugins', []) plugins = value.get('plugins', [])
plugins_are_valid = isinstance(plugins, list) and all( plugins_are_valid = isinstance(plugins, list) and all(_valid_plugin_binding(plugin) for plugin in plugins)
_valid_plugin_binding(plugin) for plugin in plugins
)
normalized['plugins'] = list(plugins) if plugins_are_valid else [] normalized['plugins'] = list(plugins) if plugins_are_valid else []
if not plugins_are_valid: if not plugins_are_valid:
normalized['enable_all_plugins'] = False normalized['enable_all_plugins'] = False
mcp_servers = value.get('mcp_servers', []) mcp_servers = value.get('mcp_servers', [])
mcp_servers_are_valid = isinstance(mcp_servers, list) and all( mcp_servers_are_valid = isinstance(mcp_servers, list) and all(_valid_name(server) for server in mcp_servers)
_valid_name(server) for server in mcp_servers
)
normalized['mcp_servers'] = list(mcp_servers) if mcp_servers_are_valid else [] normalized['mcp_servers'] = list(mcp_servers) if mcp_servers_are_valid else []
if not mcp_servers_are_valid: if not mcp_servers_are_valid:
normalized['enable_all_mcp_servers'] = False 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") raise ValueError(f"{context} field '{field_label}' must be a list")
for index, item in enumerate(items): for index, item in enumerate(items):
if not item_validator(item): if not item_validator(item):
raise ValueError( raise ValueError(f"{context} field '{field_label}[{index}]' must be {item_description}")
f"{context} field '{field_label}[{index}]' must be {item_description}"
)
@@ -65,8 +65,12 @@ class OfficialAccountMessageConverter(abstract_platform_adapter.AbstractMessageC
else: else:
components.append(platform_message.Unknown(text='[officialaccount voice message without media id]')) components.append(platform_message.Unknown(text='[officialaccount voice message without media id]'))
elif event.type == 'event': 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: 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) return platform_message.MessageChain(components)
@@ -3,4 +3,3 @@
from langbot.pkg.platform.adapters.qqofficial.adapter import QQOfficialAdapter from langbot.pkg.platform.adapters.qqofficial.adapter import QQOfficialAdapter
__all__ = ['QQOfficialAdapter'] __all__ = ['QQOfficialAdapter']
@@ -54,7 +54,9 @@ class QQOfficialAPIMixin:
self, self,
group_id: typing.Union[int, str], group_id: typing.Union[int, str],
) -> list[platform_entities.UserGroupMember]: ) -> 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( async def edit_message(
self, self,
@@ -100,4 +102,3 @@ class QQOfficialAPIMixin:
async def leave_group(self, group_id: typing.Union[int, str]): async def leave_group(self, group_id: typing.Union[int, str]):
raise NotSupportedError('leave_group') raise NotSupportedError('leave_group')
@@ -8,4 +8,3 @@ except ModuleNotFoundError:
def __init__(self, api_name: str, *args): def __init__(self, api_name: str, *args):
super().__init__(f"API '{api_name}' is not supported by this adapter", *args) super().__init__(f"API '{api_name}' is not supported by this adapter", *args)
self.api_name = api_name self.api_name = api_name
@@ -34,4 +34,3 @@ PLATFORM_API_MAP: dict[str, typing.Callable[[typing.Any, dict], typing.Awaitable
'get_gateway_url': get_gateway_url, 'get_gateway_url': get_gateway_url,
'get_mode': get_mode, 'get_mode': get_mode,
} }
@@ -47,7 +47,9 @@ class SlackAPIMixin:
self, self,
group_id: typing.Union[int, str], group_id: typing.Union[int, str],
) -> list[platform_entities.UserGroupMember]: ) -> 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( async def get_group_member_info(
self, self,
@@ -19,7 +19,9 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
async def yiri2target(event: platform_events.Event) -> typing.Any: async def yiri2target(event: platform_events.Event) -> typing.Any:
return getattr(event, 'source_platform_object', None) 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) eba_event = await self.target2yiri(event)
if not isinstance(eba_event, platform_events.MessageReceivedEvent): if not isinstance(eba_event, platform_events.MessageReceivedEvent):
return None return None
@@ -1,3 +1,3 @@
from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter
__all__ = ["TelegramAdapter"] __all__ = ['TelegramAdapter']
@@ -14,7 +14,7 @@ async def pin_message(bot: telegram.Bot, params: dict) -> dict:
message_id=params['message_id'], message_id=params['message_id'],
disable_notification=params.get('disable_notification', False), disable_notification=params.get('disable_notification', False),
) )
return {"ok": True} return {'ok': True}
async def unpin_message(bot: telegram.Bot, params: dict) -> dict: 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'], chat_id=params['chat_id'],
message_id=params.get('message_id'), message_id=params.get('message_id'),
) )
return {"ok": True} return {'ok': True}
async def unpin_all_messages(bot: telegram.Bot, params: dict) -> dict: async def unpin_all_messages(bot: telegram.Bot, params: dict) -> dict:
"""Unpin all messages in a chat.""" """Unpin all messages in a chat."""
await bot.unpin_all_chat_messages(chat_id=params['chat_id']) 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: async def get_chat_administrators(bot: telegram.Bot, params: dict) -> dict:
"""Get chat administrator list.""" """Get chat administrator list."""
admins = await bot.get_chat_administrators(chat_id=params['chat_id']) admins = await bot.get_chat_administrators(chat_id=params['chat_id'])
return { return {
"administrators": [ 'administrators': [
{ {
"user_id": a.user.id, 'user_id': a.user.id,
"username": a.user.username, 'username': a.user.username,
"first_name": a.user.first_name, 'first_name': a.user.first_name,
"status": a.status, 'status': a.status,
"custom_title": getattr(a, 'custom_title', None), 'custom_title': getattr(a, 'custom_title', None),
} }
for a in admins for a in admins
] ]
@@ -55,7 +55,7 @@ async def set_chat_title(bot: telegram.Bot, params: dict) -> dict:
chat_id=params['chat_id'], chat_id=params['chat_id'],
title=params['title'], title=params['title'],
) )
return {"ok": True} return {'ok': True}
async def set_chat_description(bot: telegram.Bot, params: dict) -> dict: 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'], chat_id=params['chat_id'],
description=params.get('description', ''), description=params.get('description', ''),
) )
return {"ok": True} return {'ok': True}
async def get_chat_member_count(bot: telegram.Bot, params: dict) -> dict: async def get_chat_member_count(bot: telegram.Bot, params: dict) -> dict:
"""Get chat member count.""" """Get chat member count."""
count = await bot.get_chat_member_count(chat_id=params['chat_id']) 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: 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'], chat_id=params['chat_id'],
action=params.get('action', 'typing'), action=params.get('action', 'typing'),
) )
return {"ok": True} return {'ok': True}
async def create_chat_invite_link(bot: telegram.Bot, params: dict) -> dict: 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'), member_limit=params.get('member_limit'),
) )
return { return {
"invite_link": link.invite_link, 'invite_link': link.invite_link,
"name": link.name, 'name': link.name,
"is_primary": link.is_primary, 'is_primary': link.is_primary,
"is_revoked": link.is_revoked, '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), show_alert=params.get('show_alert', False),
url=params.get('url'), url=params.get('url'),
) )
return {"ok": True} return {'ok': True}
# ---- Action dispatch table ---- # ---- Action dispatch table ----
PLATFORM_API_MAP: dict[str, typing.Callable[[telegram.Bot, dict], typing.Awaitable[dict]]] = { PLATFORM_API_MAP: dict[str, typing.Callable[[telegram.Bot, dict], typing.Awaitable[dict]]] = {
"pin_message": pin_message, 'pin_message': pin_message,
"unpin_message": unpin_message, 'unpin_message': unpin_message,
"unpin_all_messages": unpin_all_messages, 'unpin_all_messages': unpin_all_messages,
"get_chat_administrators": get_chat_administrators, 'get_chat_administrators': get_chat_administrators,
"set_chat_title": set_chat_title, 'set_chat_title': set_chat_title,
"set_chat_description": set_chat_description, 'set_chat_description': set_chat_description,
"get_chat_member_count": get_chat_member_count, 'get_chat_member_count': get_chat_member_count,
"send_chat_action": send_chat_action, 'send_chat_action': send_chat_action,
"create_chat_invite_link": create_chat_invite_link, 'create_chat_invite_link': create_chat_invite_link,
"answer_callback_query": answer_callback_query, 'answer_callback_query': answer_callback_query,
} }
@@ -7,7 +7,8 @@ from enum import Enum
class TelegramChatType(str, Enum): class TelegramChatType(str, Enum):
"""Telegram chat type.""" """Telegram chat type."""
PRIVATE = "private"
GROUP = "group" PRIVATE = 'private'
SUPERGROUP = "supergroup" GROUP = 'group'
CHANNEL = "channel" SUPERGROUP = 'supergroup'
CHANNEL = 'channel'
@@ -54,7 +54,9 @@ class WecomBotAPIMixin:
self, self,
group_id: typing.Union[int, str], group_id: typing.Union[int, str],
) -> list[platform_entities.UserGroupMember]: ) -> 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: async def upload_file(self, file_data: bytes, filename: str) -> str:
raise NotSupportedError('upload_file') raise NotSupportedError('upload_file')
@@ -19,7 +19,9 @@ class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter):
async def yiri2target(event: platform_events.Event) -> typing.Any: async def yiri2target(event: platform_events.Event) -> typing.Any:
return getattr(event, 'source_platform_object', None) 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) eba_event = await self.target2yiri(event)
if not isinstance(eba_event, platform_events.MessageReceivedEvent): if not isinstance(eba_event, platform_events.MessageReceivedEvent):
return None return None
@@ -50,7 +52,9 @@ class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter):
async def target2yiri(self, event: WecomBotEvent) -> platform_events.Event: async def target2yiri(self, event: WecomBotEvent) -> platform_events.Event:
if event.type in {'single', 'group'} and event.msgtype != 'event': if event.type in {'single', 'group'} and event.msgtype != 'event':
return await self.message_to_eba(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: async def message_to_eba(self, event: WecomBotEvent) -> platform_events.MessageReceivedEvent:
sender = platform_entities.User(id=event.userid, nickname=event.username or event.userid) sender = platform_entities.User(id=event.userid, nickname=event.username or event.userid)
@@ -17,7 +17,9 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter):
return getattr(event, 'source_platform_object', None) return getattr(event, 'source_platform_object', None)
@staticmethod @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) eba_event = await WecomCSEventConverter.target2yiri(event, bot)
if hasattr(eba_event, 'to_legacy_event'): if hasattr(eba_event, 'to_legacy_event'):
return 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"}') return WecomCSEventConverter.platform_specific(event, f'wecomcs.{event.type or "unknown"}')
@staticmethod @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) message_chain = await WecomCSMessageConverter.target2yiri(event)
sender = await WecomCSEventConverter.user_from_event(event, bot) sender = await WecomCSEventConverter.user_from_event(event, bot)
return platform_events.MessageReceivedEvent( return platform_events.MessageReceivedEvent(
+6 -4
View File
@@ -89,10 +89,12 @@ def format_human_input_text(
if actions: if actions:
lines.append('') lines.append('')
if input_defs: if input_defs:
lines.extend([ lines.extend(
'Reply with action plus field values to continue:', [
' action: <number or title>', 'Reply with action plus field values to continue:',
]) ' action: <number or title>',
]
)
else: else:
lines.append('Reply with the number or title to continue:') lines.append('Reply with the number or title to continue:')
for idx, action in enumerate(actions, start=1): for idx, action in enumerate(actions, start=1):
+3 -11
View File
@@ -2327,15 +2327,9 @@ class MCPLoader(loader.ToolLoader):
return items return items
def _session_by_source_id( def _session_by_source_id(self, context: TenantContext, source_id: str) -> RuntimeMCPSession | None:
self, context: TenantContext, source_id: str
) -> RuntimeMCPSession | None:
return next( 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, None,
) )
@@ -2397,9 +2391,7 @@ class MCPLoader(loader.ToolLoader):
source_id: str | None = None, source_id: str | None = None,
) -> typing.Any: ) -> typing.Any:
"""执行工具调用""" """执行工具调用"""
execution_context = await self._assert_execution_active( execution_context = await self._assert_execution_active(_execution_context_from_query(query))
_execution_context_from_query(query)
)
if source_id is None and name == MCP_TOOL_LIST_RESOURCES: 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: if getattr(query, 'variables', {}).get('_pipeline_mcp_resource_agent_read_enabled', True) is not True:
raise ToolExecutionDeniedError(name, 'MCP resource agent reads are disabled') raise ToolExecutionDeniedError(name, 'MCP resource agent reads are disabled')
+2 -6
View File
@@ -368,9 +368,7 @@ class ToolManager:
return None return None
return await self.plugin_tool_loader.get_tool(name, source_id=source_id) return await self.plugin_tool_loader.get_tool(name, source_id=source_id)
if source == 'mcp': if source == 'mcp':
return await self.mcp_tool_loader.get_tool( return await self.mcp_tool_loader.get_tool(context, name, source_id=source_id)
context, name, source_id=source_id
)
return None return None
async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list: 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: if source_ref is not None:
execution_context = get_query_execution_context(query) execution_context = get_query_execution_context(query)
await self._bind_plugin_workspace(execution_context) await self._bind_plugin_workspace(execution_context)
sandbox_available = await self._workspace_sandbox_available( sandbox_available = await self._workspace_sandbox_available(execution_context)
execution_context
)
source = source_ref['source'] source = source_ref['source']
source_id = source_ref.get('source_id') source_id = source_ref.get('source_id')
uses_source_id = False uses_source_id = False
@@ -108,7 +108,6 @@ class TestSQLiteMigrationUpgrade:
await run_alembic_upgrade(sqlite_engine, 'head') await run_alembic_upgrade(sqlite_engine, 'head')
assert await get_alembic_current(sqlite_engine) == _get_script_head() assert await get_alembic_current(sqlite_engine) == _get_script_head()
assert _get_script_head() == '0023_drop_agent_enabled'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upgrade_from_development_workspace_head_to_merged_head(self, sqlite_engine): 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') await run_alembic_upgrade(sqlite_engine, 'head')
assert await get_alembic_current(sqlite_engine) == _get_script_head() assert await get_alembic_current(sqlite_engine) == _get_script_head()
assert _get_script_head() == '0023_drop_agent_enabled'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upgrade_from_reasoning_config_head_to_merged_head(self, sqlite_engine): 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_stamp(sqlite_engine, '0018_llm_reasoning_config')
await run_alembic_upgrade(sqlite_engine, 'head') 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 @pytest.mark.asyncio
async def test_upgrade_removes_agent_enabled_column(self, sqlite_engine): async def test_upgrade_removes_agent_enabled_column(self, sqlite_engine):
+105 -37
View File
@@ -2,69 +2,137 @@ import assert from 'node:assert/strict';
import fs from 'node:fs'; import fs from 'node:fs';
import test from 'node:test'; import test from 'node:test';
import ts from 'typescript'; 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: {} }; 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 { 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', () => { test('separates streamed thinking and text, replaces final snapshot without duplication', () => {
assert.deepEqual(executionSteps([ assert.deepEqual(
event('message.delta', {chunk: {content:'<think>plan'}}), executionSteps([
event('message.delta', {chunk: {content:'</think>hello'}}), event('message.delta', { chunk: { content: '<think>plan' } }),
event('message.completed', {message: {content:'<think>plan</think>hello'}}), event('message.delta', { chunk: { content: '</think>hello' } }),
event('run.completed', {message: {content:'hello'}}), event('message.completed', {
]), [{kind:'message', text:'hello', reasoning:'plan'}]); message: { content: '<think>plan</think>hello' },
}),
event('run.completed', { message: { content: 'hello' } }),
]),
[{ kind: 'message', text: 'hello', reasoning: 'plan' }],
);
}); });
test('retains structured reasoning and tool parameters/results in order', () => { test('retains structured reasoning and tool parameters/results in order', () => {
const steps = executionSteps([ const steps = executionSteps([
event('message.delta', {chunk: {provider_specific_fields:{reasoning_content:'plan'}}}), event('message.delta', {
event('message.completed', {message: {content:''}}), chunk: { provider_specific_fields: { reasoning_content: 'plan' } },
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('message.completed', { message: { content: '' } }),
event('tool.call.completed', {tool_call_id:'2',tool_name:'exec', error:'failed'}), event('tool.call.started', {
event('tool.call.completed', {tool_call_id:'1',tool_name:'exec', result:{stdout:'hi'}}), 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', {}), event('run.failed', {}),
]); ]);
assert.equal(steps[0].reasoning, 'plan'); assert.equal(steps[0].reasoning, 'plan');
assert.equal(steps[1].parameters.command, 'echo hi'); 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].status, 'failed');
assert.equal(steps[2].error, 'failed'); assert.equal(steps[2].error, 'failed');
}); });
test('replaces LocalAgent cumulative snapshots instead of repeating text', () => { test('replaces LocalAgent cumulative snapshots instead of repeating text', () => {
assert.deepEqual(executionSteps([ assert.deepEqual(
event('message.delta', {chunk: {content:'hello', msg_sequence:1}}), executionSteps([
event('message.delta', {chunk: {content:'hello world', msg_sequence:2}}), event('message.delta', { chunk: { content: 'hello', msg_sequence: 1 } }),
event('message.delta', {chunk: {content:'hello world', msg_sequence:3, is_final:true}}), event('message.delta', {
]), [{kind:'message', text:'hello world', reasoning:''}]); 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', () => { test('shows failed tool results even when the call transport completed', () => {
const steps = executionSteps([ const steps = executionSteps([
event('tool.call.started', {tool_call_id:'exit7', tool_name:'exec', parameters:{command:'exit 7'}}), event('tool.call.started', {
event('tool.call.completed', {tool_call_id:'exit7', tool_name:'exec', result:{ok:false, exit_code:7, stderr:'expected'}}), 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].status, 'failed');
assert.equal(steps[0].result.exit_code,7); assert.equal(steps[0].result.exit_code, 7);
}); });
test('does not repeat prior thinking across LocalAgent tool turns', () => { test('does not repeat prior thinking across LocalAgent tool turns', () => {
const prefix = '<think>first thought</think>'; const prefix = '<think>first thought</think>';
const steps = executionSteps([ const steps = executionSteps([
event('message.delta', {chunk:{content:prefix, msg_sequence:1}}), 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.started', {
event('tool.call.completed', {tool_call_id:'w',tool_name:'write',result:{ok:true}}), tool_call_id: 'w',
event('message.delta', {chunk:{content:prefix+'now read',msg_sequence:1}}), tool_name: 'write',
event('tool.call.started', {tool_call_id:'r',tool_name:'read'}), parameters: { path: '/workspace/a' },
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('tool.call.completed', {
event('message.completed', {message:{content:'done'}}), 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, [ assert.deepEqual(messages, [
{kind:'message',text:'',reasoning:'first thought'}, { kind: 'message', text: '', reasoning: 'first thought' },
{kind:'message',text:'now read',reasoning:''}, { kind: 'message', text: 'now read', reasoning: '' },
{kind:'message',text:'done',reasoning:''}, { kind: 'message', text: 'done', reasoning: '' },
]); ]);
}); });