From 1803fb38ca79412cacbb49c8e906b0ae4e9372b3 Mon Sep 17 00:00:00 2001 From: GuoQing Zhang Date: Mon, 13 Jul 2026 20:07:40 +0800 Subject: [PATCH] fix: include tool identity in assistant onboarding errors --- src/backend/bisheng/api/services/assistant.py | 2 +- .../bisheng/tool/domain/services/executor.py | 42 +++++++- .../tool/test_tool_executor_permissions.py | 99 ++++++++++++++++--- 3 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src/backend/bisheng/api/services/assistant.py b/src/backend/bisheng/api/services/assistant.py index 01a544647..91f2d1aaa 100644 --- a/src/backend/bisheng/api/services/assistant.py +++ b/src/backend/bisheng/api/services/assistant.py @@ -588,7 +588,7 @@ class AssistantService(BaseService, AssistantUtils): await tmp_agent.init_assistant() except Exception as e: logger.exception("online agent init failed") - raise AssistantInitError(exception=e) + raise AssistantInitError(exception=e, msg=f"Assistant onboarding failed: {e}") from e assistant.status = status AssistantDao.update_assistant(assistant) await telemetry_service.log_event( diff --git a/src/backend/bisheng/tool/domain/services/executor.py b/src/backend/bisheng/tool/domain/services/executor.py index 49fcc92ed..b37543153 100644 --- a/src/backend/bisheng/tool/domain/services/executor.py +++ b/src/backend/bisheng/tool/domain/services/executor.py @@ -28,6 +28,15 @@ from bisheng_langchain.gpts.load_tools import load_tools from bisheng_langchain.gpts.tools.api_tools.openapi import OpenApiTools +class ToolInitializationError(RuntimeError): + """Add a safe tool identity to an initialization failure.""" + + def __init__(self, tool: GptsTools, cause: Exception): + tool_id = getattr(tool, "id", None) + tool_name = getattr(tool, "name", None) or getattr(tool, "tool_key", None) or "unknown" + super().__init__(f'Tool "{tool_name}" (id={tool_id}) initialization failed: {type(cause).__name__}: {cause}') + + def wrapper_tool(func): @functools.wraps(func) async def inner(*args, **kwargs): @@ -223,6 +232,33 @@ class ToolExecutor(BaseTool): base_tool=tool_instance, tool=tool, app_id=app_id, app_name=app_name, app_type=app_type, user_id=user_id ) + @classmethod + def _init_by_tool_and_type_with_context( + cls, + tool: GptsTools, + tool_type: GptsToolsType, + *, + app_id: str, + app_name: str, + app_type: ApplicationTypeEnum, + user_id: int, + **kwargs, + ) -> BaseTool: + try: + return cls._init_by_tool_and_type( + tool=tool, + tool_type=tool_type, + app_id=app_id, + app_name=app_name, + app_type=app_type, + user_id=user_id, + **kwargs, + ) + except ToolInitializationError: + raise + except Exception as exc: + raise ToolInitializationError(tool, exc) from exc + @classmethod async def init_by_tool_id( cls, @@ -245,7 +281,7 @@ class ToolExecutor(BaseTool): if not tool_type: raise ValueError(f"Tool type with id {tool.type} not found.") await cls._ensure_use_permission_async(tool_type, user_id) - return cls._init_by_tool_and_type( + return cls._init_by_tool_and_type_with_context( tool=tool, tool_type=tool_type, app_id=app_id, @@ -276,7 +312,7 @@ class ToolExecutor(BaseTool): if enforce_permission: cls._ensure_use_permission_sync(tool_type, user_id) result.append( - cls._init_by_tool_and_type( + cls._init_by_tool_and_type_with_context( tool=tool, tool_type=tool_type, app_id=app_id, @@ -353,7 +389,7 @@ class ToolExecutor(BaseTool): raise ValueError(f"Tool type with id {tool.type} not found.") cls._ensure_use_permission_sync(tool_type, user_id) - return cls._init_by_tool_and_type( + return cls._init_by_tool_and_type_with_context( tool=tool, tool_type=tool_type, app_id=app_id, diff --git a/src/backend/test/tool/test_tool_executor_permissions.py b/src/backend/test/tool/test_tool_executor_permissions.py index d7de99ef0..334f5cc83 100644 --- a/src/backend/test/tool/test_tool_executor_permissions.py +++ b/src/backend/test/tool/test_tool_executor_permissions.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock import pytest from bisheng.common.constants.enums.telemetry import ApplicationTypeEnum -from bisheng.tool.domain.services.executor import ToolExecutor +from bisheng.tool.domain.services.executor import ToolExecutor, ToolInitializationError @pytest.mark.asyncio @@ -15,35 +15,35 @@ async def test_init_by_tool_ids_can_skip_unauthorized_tools(monkeypatch): type_two = SimpleNamespace(id=20) monkeypatch.setattr( - 'bisheng.tool.domain.services.executor.GptsToolsDao.aget_list_by_ids', + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_list_by_ids", AsyncMock(return_value=[tool_one, tool_two]), ) monkeypatch.setattr( - 'bisheng.tool.domain.services.executor.GptsToolsDao.aget_all_tool_type', + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_all_tool_type", AsyncMock(return_value=[type_one, type_two]), ) async def ensure_permission(tool_type, user_id): if tool_type.id == 20: - raise PermissionError('no tool permission') + raise PermissionError("no tool permission") - monkeypatch.setattr(ToolExecutor, '_ensure_use_permission_async', ensure_permission) + monkeypatch.setattr(ToolExecutor, "_ensure_use_permission_async", ensure_permission) monkeypatch.setattr( ToolExecutor, - '_init_by_tool_and_type', - lambda tool, tool_type, **kwargs: f'tool:{tool.id}', + "_init_by_tool_and_type", + lambda tool, tool_type, **kwargs: f"tool:{tool.id}", ) result = await ToolExecutor.init_by_tool_ids( [1, 2], - app_id='assistant-1', - app_name='assistant', + app_id="assistant-1", + app_name="assistant", app_type=ApplicationTypeEnum.ASSISTANT, user_id=7, skip_unauthorized=True, ) - assert result == ['tool:1'] + assert result == ["tool:1"] @pytest.mark.asyncio @@ -52,24 +52,91 @@ async def test_init_by_tool_ids_still_raises_by_default(monkeypatch): tool_type = SimpleNamespace(id=20) monkeypatch.setattr( - 'bisheng.tool.domain.services.executor.GptsToolsDao.aget_list_by_ids', + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_list_by_ids", AsyncMock(return_value=[tool]), ) monkeypatch.setattr( - 'bisheng.tool.domain.services.executor.GptsToolsDao.aget_all_tool_type', + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_all_tool_type", AsyncMock(return_value=[tool_type]), ) async def ensure_permission(tool_type, user_id): - raise PermissionError('no tool permission') + raise PermissionError("no tool permission") - monkeypatch.setattr(ToolExecutor, '_ensure_use_permission_async', ensure_permission) + monkeypatch.setattr(ToolExecutor, "_ensure_use_permission_async", ensure_permission) with pytest.raises(PermissionError): await ToolExecutor.init_by_tool_ids( [2], - app_id='direct-tool-use', - app_name='direct', + app_id="direct-tool-use", + app_name="direct", app_type=ApplicationTypeEnum.ASSISTANT, user_id=7, ) + + +@pytest.mark.asyncio +async def test_init_by_tool_ids_error_includes_tool_identity(monkeypatch): + tool = SimpleNamespace(id=3, type=30, name="broken-api-tool") + tool_type = SimpleNamespace(id=30) + + monkeypatch.setattr( + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_list_by_ids", + AsyncMock(return_value=[tool]), + ) + monkeypatch.setattr( + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_all_tool_type", + AsyncMock(return_value=[tool_type]), + ) + monkeypatch.setattr(ToolExecutor, "_ensure_use_permission_async", AsyncMock()) + + def fail_initialization(*args, **kwargs): + raise KeyError("properties") + + monkeypatch.setattr(ToolExecutor, "_init_by_tool_and_type", fail_initialization) + + with pytest.raises(ToolInitializationError) as exc_info: + await ToolExecutor.init_by_tool_ids( + [3], + app_id="assistant-1", + app_name="assistant", + app_type=ApplicationTypeEnum.ASSISTANT, + user_id=7, + ) + + assert str(exc_info.value) == ("Tool \"broken-api-tool\" (id=3) initialization failed: KeyError: 'properties'") + assert isinstance(exc_info.value.__cause__, KeyError) + + +@pytest.mark.asyncio +async def test_init_by_tool_id_error_includes_tool_identity(monkeypatch): + tool = SimpleNamespace(id=4, type=40, name="single-broken-tool") + tool_type = SimpleNamespace(id=40) + + monkeypatch.setattr( + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_one_tool", + AsyncMock(return_value=tool), + ) + monkeypatch.setattr( + "bisheng.tool.domain.services.executor.GptsToolsDao.aget_one_tool_type", + AsyncMock(return_value=tool_type), + ) + monkeypatch.setattr(ToolExecutor, "_ensure_use_permission_async", AsyncMock()) + + def fail_initialization(*args, **kwargs): + raise ValueError("invalid configuration") + + monkeypatch.setattr(ToolExecutor, "_init_by_tool_and_type", fail_initialization) + + with pytest.raises(ToolInitializationError) as exc_info: + await ToolExecutor.init_by_tool_id( + tool_id=4, + app_id="assistant-1", + app_name="assistant", + app_type=ApplicationTypeEnum.ASSISTANT, + user_id=7, + ) + + assert str(exc_info.value) == ( + 'Tool "single-broken-tool" (id=4) initialization failed: ValueError: invalid configuration' + )