diff --git a/api/services/skill_management_service.py b/api/services/skill_management_service.py index 2343481ab2b..bf2f1bb6d5c 100644 --- a/api/services/skill_management_service.py +++ b/api/services/skill_management_service.py @@ -48,9 +48,6 @@ from models.agent import ( AgentConfigRevisionOperation, AgentConfigSnapshot, AgentKind, - AgentRuntimeSession, - AgentRuntimeSessionOwnerType, - AgentRuntimeSessionStatus, AgentScope, AgentSource, AgentStatus, @@ -1062,7 +1059,6 @@ class SkillManagementService: model_config: AgentSoulModelConfig, ) -> None: model_changed = False - cleaned_runtime_session_count = 0 if assistant.active_config_snapshot_id: snapshot = session.get(AgentConfigSnapshot, assistant.active_config_snapshot_id) if snapshot is not None: @@ -1094,24 +1090,11 @@ class SkillManagementService: draft_config.model = model_config draft.config_snapshot = draft_config if model_changed: - for runtime_session in session.scalars( - select(AgentRuntimeSession).where( - AgentRuntimeSession.owner_type == AgentRuntimeSessionOwnerType.CONVERSATION, - AgentRuntimeSession.tenant_id == assistant.tenant_id, - AgentRuntimeSession.app_id == assistant.backing_app_id, - AgentRuntimeSession.agent_id == assistant.id, - AgentRuntimeSession.status == AgentRuntimeSessionStatus.ACTIVE, - ) - ): - runtime_session.status = AgentRuntimeSessionStatus.CLEANED - runtime_session.cleaned_at = naive_utc_now() - cleaned_runtime_session_count += 1 logger.info( - "skill_assistant_model_synced assistant_id=%s provider=%s model=%s cleaned_runtime_sessions=%s", + "skill_assistant_model_synced assistant_id=%s provider=%s model=%s", assistant.id, model_config.model_provider, model_config.model, - cleaned_runtime_session_count, ) def update_metadata( diff --git a/api/tests/unit_tests/controllers/console/workspace/test_skills.py b/api/tests/unit_tests/controllers/console/workspace/test_skills.py index 6eb0b35d210..b0ae0abcecc 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_skills.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_skills.py @@ -33,7 +33,7 @@ def current_user() -> Account: return user -def _skill_detail() -> dict: +def _skill_detail() -> dict[str, object]: return { "id": "skill-1", "name": "finance-sop", @@ -92,13 +92,14 @@ def test_list_skills_uses_default_pagination_when_query_omits_page_and_limit(app api = WorkspaceSkillsApi() method = unwrap(api.get) service = MagicMock() - service.list_skills.return_value = { + list_response: dict[str, object] = { "data": [], "has_more": False, "limit": 20, "page": 1, "total": 0, } + service.list_skills.return_value = list_response with ( app.test_request_context("/?keyword=finance&tag=ops&tag=", method="GET"), @@ -236,7 +237,7 @@ def test_get_skill_version_returns_version_detail(app: Flask) -> None: api = WorkspaceSkillVersionApi() method = unwrap(api.get) service = MagicMock() - service.get_version.return_value = { + version_response: dict[str, object] = { "id": "version-1", "skill_id": "skill-1", "version_number": 1, @@ -262,6 +263,7 @@ def test_get_skill_version_returns_version_detail(app: Flask) -> None: } ], } + service.get_version.return_value = version_response with ( app.test_request_context("/", method="GET"), @@ -341,11 +343,8 @@ def test_skill_assistant_runs_agent_app_stream(app: Flask, current_user: Account api = WorkspaceSkillAssistMessageApi() method = unwrap(api.post) service = MagicMock() - assistant_app = MagicMock() - assistant_app.id = "assistant-app-1" - service.get_or_create_assistant_app.return_value = (assistant_app, "draft") - app_model = MagicMock() - app_response = MagicMock() + action_stream = MagicMock() + service.create_assistant_action_stream.return_value = action_stream compact_response = MagicMock() with ( @@ -368,19 +367,18 @@ def test_skill_assistant_runs_agent_app_stream(app: Flask, current_user: Account ), patch("controllers.console.workspace.skills.SkillManagementService", return_value=service), patch( - "controllers.console.workspace.skills.db.session", - return_value=MagicMock(get=MagicMock(return_value=app_model)), - ), - patch("controllers.console.workspace.skills.AppGenerateService.generate", return_value=app_response), - patch("controllers.console.workspace.skills.helper.compact_generate_response", return_value=compact_response), + "controllers.console.workspace.skills.helper.compact_generate_response", + return_value=compact_response, + ) as compact_generate_response, ): response = method(api, "tenant-1", current_user, "skill-1") assert response is compact_response - service.get_or_create_assistant_app.assert_called_once_with( + service.create_assistant_action_stream.assert_called_once_with( tenant_id="tenant-1", skill_id="skill-1", user_id="user-1", + message="Create an approval checklist.", attachments=[ SkillAssistAttachmentPayload( tool_file_id="tool-file-1", @@ -389,6 +387,7 @@ def test_skill_assistant_runs_agent_app_stream(app: Flask, current_user: Account size=128, ) ], - message="Create an approval checklist.", model_payload=None, + target_path=None, ) + compact_generate_response.assert_called_once_with(action_stream) diff --git a/api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py b/api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py index 47a4338048f..04beeffa6b8 100644 --- a/api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py @@ -34,7 +34,7 @@ from models.agent_config_entities import AgentSoulConfig def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( "core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs", - lambda *, tenant_id, agent_id: [], + lambda **_kwargs: [], ) @@ -523,7 +523,7 @@ class TestAgentAppConfigLayer: def test_config_layer_includes_bound_workspace_skills(self, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( "core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs", - lambda *, tenant_id, agent_id: [ + lambda **_kwargs: [ DifyConfigSkillConfig( name="workspace-skill", description="Bound workspace skill.", diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index 9f2927ba889..72c96e33752 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -45,7 +45,7 @@ from models.agent_config_entities import ( def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( "core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs", - lambda *, tenant_id, agent_id: [], + lambda **_kwargs: [], ) @@ -1541,7 +1541,7 @@ def test_workflow_run_request_contains_config_layer(): def test_workflow_run_request_includes_bound_workspace_skills(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( "core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs", - lambda *, tenant_id, agent_id: [ + lambda **_kwargs: [ DifyConfigSkillConfig( name="workspace-skill", description="Bound workspace skill.", diff --git a/api/tests/unit_tests/services/test_skill_management_service.py b/api/tests/unit_tests/services/test_skill_management_service.py index 0bfa9d9ad79..98f72f42e5e 100644 --- a/api/tests/unit_tests/services/test_skill_management_service.py +++ b/api/tests/unit_tests/services/test_skill_management_service.py @@ -7,13 +7,15 @@ import json import zipfile from collections.abc import Generator from types import SimpleNamespace +from typing import cast, override from unittest.mock import patch from uuid import uuid4 import pytest -from sqlalchemy import delete, func, select +from sqlalchemy import Table, delete, func, select from core.db.session_factory import session_factory +from core.tools.tool_file_manager import ToolFileManager from models.account import Account from models.agent import ( Agent, @@ -22,9 +24,6 @@ from models.agent import ( AgentConfigRevision, AgentConfigSnapshot, AgentKind, - AgentRuntimeSession, - AgentRuntimeSessionOwnerType, - AgentRuntimeSessionStatus, AgentScope, AgentSource, AgentStatus, @@ -62,27 +61,33 @@ AGENT = "22222222-2222-2222-2222-222222222222" USER = "33333333-3333-3333-3333-333333333333" -class _FakeToolFileManager: - def create_file_by_raw(self, **kwargs): +class _FakeToolFileManager(ToolFileManager): + @override + def create_file_by_raw( + self, + *, + user_id: str, + tenant_id: str, + conversation_id: str | None, + file_binary: bytes, + mimetype: str, + filename: str | None = None, + ) -> ToolFile: tool_file = ToolFile( - user_id=kwargs["user_id"], - tenant_id=kwargs["tenant_id"], - conversation_id=kwargs["conversation_id"], + user_id=user_id, + tenant_id=tenant_id, + conversation_id=conversation_id, file_key=f"tools/{uuid4().hex}", - mimetype=kwargs["mimetype"], + mimetype=mimetype, original_url=None, - name=kwargs.get("filename") or "file.bin", - size=len(kwargs["file_binary"]), + name=filename or "file.bin", + size=len(file_binary), ) tool_file.id = str(uuid4()) with session_factory.create_session() as session: session.add(tool_file) session.commit() - return SimpleNamespace( - id=tool_file.id, - size=len(kwargs["file_binary"]), - mimetype=kwargs["mimetype"], - ) + return tool_file @pytest.fixture(autouse=True) @@ -95,7 +100,6 @@ def _tables() -> Generator[None, None, None]: AgentConfigSnapshot, AgentConfigDraft, AgentConfigRevision, - AgentRuntimeSession, ToolFile, Tag, TagBinding, @@ -106,7 +110,8 @@ def _tables() -> Generator[None, None, None]: WorkflowAgentNodeBinding, ) for model in models: - model.__table__.create(bind=engine, checkfirst=True) + table = cast(Table, model.__table__) + table.create(bind=engine, checkfirst=True) _seed_agent() yield with session_factory.create_session() as session: @@ -455,17 +460,6 @@ def test_sync_assistant_model_config_updates_debugger_draft() -> None: updated_by=USER, ) session.add(draft) - runtime_session = AgentRuntimeSession( - tenant_id=TENANT, - app_id=agent.backing_app_id, - owner_type=AgentRuntimeSessionOwnerType.CONVERSATION, - agent_id=agent.id, - agent_config_snapshot_id=draft.id, - conversation_id=str(uuid4()), - session_snapshot="{}", - status=AgentRuntimeSessionStatus.ACTIVE, - ) - session.add(runtime_session) session.flush() SkillManagementService._sync_assistant_model_config( @@ -479,7 +473,6 @@ def test_sync_assistant_model_config_updates_debugger_draft() -> None: assert updated_draft.model is not None assert updated_draft.model.model_provider == "langgenius/tongyi/tongyi" assert updated_draft.model.model == "qwen3.7-plus" - assert runtime_session.status == AgentRuntimeSessionStatus.CLEANED def test_sync_assistant_model_config_updates_draft_without_active_snapshot() -> None: @@ -1148,9 +1141,26 @@ def test_publish_archive_contains_synced_skill_md() -> None: captured: dict[str, bytes] = {} class CapturingToolFileManager(_FakeToolFileManager): - def create_file_by_raw(self, **kwargs): - captured["archive"] = kwargs["file_binary"] - return super().create_file_by_raw(**kwargs) + @override + def create_file_by_raw( + self, + *, + user_id: str, + tenant_id: str, + conversation_id: str | None, + file_binary: bytes, + mimetype: str, + filename: str | None = None, + ) -> ToolFile: + captured["archive"] = file_binary + return super().create_file_by_raw( + user_id=user_id, + tenant_id=tenant_id, + conversation_id=conversation_id, + file_binary=file_binary, + mimetype=mimetype, + filename=filename, + ) service = SkillManagementService(tool_file_manager=CapturingToolFileManager()) created = service.create_skill( @@ -1173,9 +1183,26 @@ def test_list_versions_includes_publisher_name_and_version_detail_files() -> Non captured: dict[str, bytes] = {} class CapturingToolFileManager(_FakeToolFileManager): - def create_file_by_raw(self, **kwargs): - captured["archive"] = kwargs["file_binary"] - return super().create_file_by_raw(**kwargs) + @override + def create_file_by_raw( + self, + *, + user_id: str, + tenant_id: str, + conversation_id: str | None, + file_binary: bytes, + mimetype: str, + filename: str | None = None, + ) -> ToolFile: + captured["archive"] = file_binary + return super().create_file_by_raw( + user_id=user_id, + tenant_id=tenant_id, + conversation_id=conversation_id, + file_binary=file_binary, + mimetype=mimetype, + filename=filename, + ) service = SkillManagementService(tool_file_manager=CapturingToolFileManager()) created = service.create_skill( @@ -1607,9 +1634,26 @@ def test_duplicate_skill_copies_latest_published_content_without_history() -> No captured: dict[str, bytes] = {} class CapturingToolFileManager(_FakeToolFileManager): - def create_file_by_raw(self, **kwargs): - captured["archive"] = kwargs["file_binary"] - return super().create_file_by_raw(**kwargs) + @override + def create_file_by_raw( + self, + *, + user_id: str, + tenant_id: str, + conversation_id: str | None, + file_binary: bytes, + mimetype: str, + filename: str | None = None, + ) -> ToolFile: + captured["archive"] = file_binary + return super().create_file_by_raw( + user_id=user_id, + tenant_id=tenant_id, + conversation_id=conversation_id, + file_binary=file_binary, + mimetype=mimetype, + filename=filename, + ) service = SkillManagementService(tool_file_manager=CapturingToolFileManager()) created = service.create_skill( @@ -1834,9 +1878,26 @@ def test_publish_and_export_include_binary_tool_files() -> None: captured: dict[str, bytes] = {} class CapturingToolFileManager(_FakeToolFileManager): - def create_file_by_raw(self, **kwargs): - captured["archive"] = kwargs["file_binary"] - return super().create_file_by_raw(**kwargs) + @override + def create_file_by_raw( + self, + *, + user_id: str, + tenant_id: str, + conversation_id: str | None, + file_binary: bytes, + mimetype: str, + filename: str | None = None, + ) -> ToolFile: + captured["archive"] = file_binary + return super().create_file_by_raw( + user_id=user_id, + tenant_id=tenant_id, + conversation_id=conversation_id, + file_binary=file_binary, + mimetype=mimetype, + filename=filename, + ) service = SkillManagementService(tool_file_manager=CapturingToolFileManager()) created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop")) @@ -1883,9 +1944,26 @@ def test_restore_version_replaces_draft_and_creates_new_published_version() -> N captured: list[bytes] = [] class CapturingToolFileManager(_FakeToolFileManager): - def create_file_by_raw(self, **kwargs): - captured.append(kwargs["file_binary"]) - return super().create_file_by_raw(**kwargs) + @override + def create_file_by_raw( + self, + *, + user_id: str, + tenant_id: str, + conversation_id: str | None, + file_binary: bytes, + mimetype: str, + filename: str | None = None, + ) -> ToolFile: + captured.append(file_binary) + return super().create_file_by_raw( + user_id=user_id, + tenant_id=tenant_id, + conversation_id=conversation_id, + file_binary=file_binary, + mimetype=mimetype, + filename=filename, + ) service = SkillManagementService(tool_file_manager=CapturingToolFileManager()) created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))