fix: include tool identity in assistant onboarding errors

This commit is contained in:
GuoQing Zhang
2026-07-13 20:07:40 +08:00
parent 0aa82ba942
commit 1803fb38ca
3 changed files with 123 additions and 20 deletions
@@ -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(
@@ -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,
@@ -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'
)