chore(agent-v2): sync changes (#38442)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: zyssyz123 <916125788@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: 林玮 (Jade Lin) <linw1995@icloud.com>
Co-authored-by: 盐粒 Yanli <mail@yanli.one>
This commit is contained in:
authored and GitHub committed 2026-07-06 13:51:33 +00:00
1 parent bdb3469ca0
commit d0ea5a5e0d
180 files changed
+4929 -1512

No files matched your search

@@ -0,0 +1,156 @@
from types import SimpleNamespace
import pytest
from controllers.common import agent_app_parameters
from controllers.common.agent_app_parameters import get_published_agent_app_feature_dict_and_user_input_form
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
def test_published_agent_app_parameters_use_soul_file_upload(monkeypatch):
app_model_config = SimpleNamespace(
to_dict=lambda: {
"opening_statement": "Hi from legacy presentation config",
"file_upload": {
"enabled": False,
"image": {"enabled": False},
},
}
)
app_model = SimpleNamespace(
tenant_id="tenant-1",
bound_agent_id="agent-1",
app_model_config=app_model_config,
)
agent = SimpleNamespace(
id="agent-1",
active_config_snapshot_id="snapshot-1",
active_config_is_published=True,
)
snapshot = SimpleNamespace(
config_snapshot_dict={
"app_features": {
"file_upload": {
"enabled": True,
"allowed_file_extensions": ["PNG"],
"allowed_file_types": ["image"],
"allowed_file_upload_methods": ["local_file"],
"image": {"enabled": True},
"number_limits": 2,
}
},
"app_variables": [{"name": "topic", "type": "string", "required": True}],
}
)
query_results = iter([agent, snapshot])
monkeypatch.setattr(agent_app_parameters.db.session, "scalar", lambda _: next(query_results))
features_dict, user_input_form = get_published_agent_app_feature_dict_and_user_input_form(app_model)
parameters = get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form)
assert parameters["opening_statement"] == "Hi from legacy presentation config"
assert parameters["file_upload"] == {
"enabled": True,
"allowed_file_extensions": ["PNG"],
"allowed_file_types": ["image"],
"allowed_file_upload_methods": ["local_file"],
"image": {"enabled": True},
"number_limits": 2,
}
assert parameters["user_input_form"] == [{"text-input": {"label": "topic", "variable": "topic", "required": True}}]
def test_published_agent_app_parameters_requires_bound_agent():
app_model = SimpleNamespace(
tenant_id="tenant-1",
bound_agent_id=None,
app_model_config=None,
)
with pytest.raises(AgentAppGeneratorError, match="no bound Agent"):
get_published_agent_app_feature_dict_and_user_input_form(app_model)
def test_published_agent_app_parameters_requires_existing_active_agent(monkeypatch):
app_model = SimpleNamespace(
tenant_id="tenant-1",
bound_agent_id="agent-1",
app_model_config=None,
)
monkeypatch.setattr(agent_app_parameters.db.session, "scalar", lambda _: None)
with pytest.raises(AgentAppGeneratorError, match="no bound Agent"):
get_published_agent_app_feature_dict_and_user_input_form(app_model)
@pytest.mark.parametrize(
("active_config_snapshot_id", "active_config_is_published"),
[
(None, True),
("snapshot-1", False),
],
)
def test_published_agent_app_parameters_requires_published_agent(
monkeypatch, active_config_snapshot_id, active_config_is_published
):
app_model = SimpleNamespace(
tenant_id="tenant-1",
bound_agent_id="agent-1",
app_model_config=None,
)
agent = SimpleNamespace(
id="agent-1",
active_config_snapshot_id=active_config_snapshot_id,
active_config_is_published=active_config_is_published,
)
monkeypatch.setattr(agent_app_parameters.db.session, "scalar", lambda _: agent)
with pytest.raises(AgentAppNotPublishedError, match="not been published"):
get_published_agent_app_feature_dict_and_user_input_form(app_model)
def test_published_agent_app_parameters_requires_published_snapshot(monkeypatch):
app_model = SimpleNamespace(
tenant_id="tenant-1",
bound_agent_id="agent-1",
app_model_config=None,
)
agent = SimpleNamespace(
id="agent-1",
active_config_snapshot_id="snapshot-1",
active_config_is_published=True,
)
query_results = iter([agent, None])
monkeypatch.setattr(agent_app_parameters.db.session, "scalar", lambda _: next(query_results))
with pytest.raises(AgentAppGeneratorError, match="published version not found"):
get_published_agent_app_feature_dict_and_user_input_form(app_model)
def test_published_agent_app_parameters_allows_missing_legacy_app_model_config(monkeypatch):
app_model = SimpleNamespace(
tenant_id="tenant-1",
bound_agent_id="agent-1",
app_model_config=None,
)
agent = SimpleNamespace(
id="agent-1",
active_config_snapshot_id="snapshot-1",
active_config_is_published=True,
)
snapshot = SimpleNamespace(config_snapshot_dict={})
query_results = iter([agent, snapshot])
monkeypatch.setattr(agent_app_parameters.db.session, "scalar", lambda _: next(query_results))
features_dict, user_input_form = get_published_agent_app_feature_dict_and_user_input_form(app_model)
assert features_dict["file_upload"] == {
"allowed_file_extensions": ["JPG", "JPEG", "PNG", "GIF", "WEBP", "SVG"],
"allowed_file_types": ["document", "image", "audio", "video"],
"allowed_file_upload_methods": ["local_file", "remote_url"],
"enabled": True,
"image": {"enabled": True},
"number_limits": 3,
}
assert user_input_form == []
@@ -5,11 +5,11 @@ from types import SimpleNamespace
import pytest
from dify_agent.client import DifyAgentClientError, DifyAgentHTTPError, DifyAgentTimeoutError
from dify_agent.protocol import SandboxListResponse, SandboxReadResponse, SandboxUploadResponse
from dify_agent.protocol import SandboxListResponse, SandboxReadResponse
from controllers.console import agent_app_sandbox as module
from models.model import App, AppMode, IconType
from services.agent_app_sandbox_service import AgentSandboxInfo, AgentSandboxInspectorError
from services.agent_app_sandbox_service import AgentSandboxInfo, AgentSandboxInspectorError, AgentSandboxUploadDownload
class _AgentAppService:
@@ -28,11 +28,11 @@ class _AgentAppService:
self.calls.append(("read", tenant_id, app_id, conversation_id, path))
return SandboxReadResponse(path=path, size=5, truncated=False, binary=False, text="hello")
def upload_file(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str) -> SandboxUploadResponse:
def upload_file(
self, *, tenant_id: str, app_id: str, conversation_id: str, path: str
) -> AgentSandboxUploadDownload:
self.calls.append(("upload", tenant_id, app_id, conversation_id, path))
return SandboxUploadResponse(
path=path, file={"transfer_method": "tool_file", "reference": "dify-file-ref:file-1"}
)
return AgentSandboxUploadDownload(url="https://files.example/report.txt")
class _WorkflowService:
@@ -74,11 +74,9 @@ class _WorkflowService:
node_id: str,
node_execution_id: str | None,
path: str,
) -> SandboxUploadResponse:
) -> AgentSandboxUploadDownload:
self.calls.append(("upload", tenant_id, app_id, workflow_run_id, node_id, node_execution_id, path))
return SandboxUploadResponse(
path=path, file={"transfer_method": "tool_file", "reference": "dify-file-ref:file-1"}
)
return AgentSandboxUploadDownload(url="https://files.example/upload.txt")
def _app_model(app_id: str = "app-1") -> App:
@@ -143,7 +141,7 @@ def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPat
assert info == {"session_id": "abc1234", "workspace_cwd": "~/workspace/abc1234"}
assert listing["path"] == "sub/report.txt"
assert preview["text"] == "hello"
assert upload["file"]["reference"] == "dify-file-ref:file-1"
assert upload == {"url": "https://files.example/report.txt"}
assert service.calls == [
("info", "tenant-1", "app-1", "conv-1", ""),
("list", "tenant-1", "app-1", "conv-1", "sub/report.txt"),
@@ -203,7 +201,7 @@ def test_workflow_agent_sandbox_resources_proxy_service(monkeypatch: pytest.Monk
assert listing["path"] == "out.txt"
assert preview["text"] == "hello"
assert upload["file"]["reference"] == "dify-file-ref:file-1"
assert upload == {"url": "https://files.example/upload.txt"}
assert service.calls == [
("list", "tenant-1", "app-1", "run-1", "agent-node", "exec-1", "out.txt"),
("read", "tenant-1", "app-1", "run-1", "agent-node", "exec-1", "out.txt"),
@@ -35,6 +35,7 @@ from controllers.openapi._errors import (
RecipientSurfaceMismatch,
)
from controllers.service_api.app.error import (
AgentNotPublishedError,
AppUnavailableError,
CompletionRequestError,
ConversationCompletedError,
@@ -306,6 +307,7 @@ ERROR_MATRIX = [
(InternalServerError(), 500, "internal_server_error"),
(BadGateway("x"), 502, "bad_gateway"),
(AppUnavailableError(), 400, "app_unavailable"),
(AgentNotPublishedError(), 400, "agent_not_published"),
(ConversationCompletedError(), 400, "conversation_completed"),
(ProviderNotInitializeError(), 400, "provider_not_initialize"),
(ProviderQuotaExceededError(), 400, "provider_quota_exceeded"),
@@ -9,7 +9,8 @@ import pytest
from flask import Flask
from controllers.service_api.app.app import AppInfoApi, AppMetaApi, AppParameterApi
from controllers.service_api.app.error import AppUnavailableError
from controllers.service_api.app.error import AgentNotPublishedError, AppUnavailableError
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
from models.account import TenantStatus
from models.model import App, AppMode
from tests.unit_tests.conftest import setup_mock_tenant_owner_execute_result
@@ -185,6 +186,41 @@ class TestAppParameterApi:
]
mock_get_agent_parameters.assert_called_once_with(mock_app_model)
@patch("controllers.service_api.wraps.user_logged_in")
@patch("controllers.service_api.wraps.current_app")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
@patch(
"controllers.service_api.app.app.get_published_agent_app_feature_dict_and_user_input_form",
side_effect=AgentAppNotPublishedError("Agent has not been published"),
)
def test_get_parameters_for_unpublished_agent_app_raises_friendly_error(
self,
mock_get_agent_parameters,
mock_db,
mock_validate_token,
mock_current_app,
mock_user_logged_in,
app: Flask,
mock_app_model,
):
_configure_current_app_mock(mock_current_app)
mock_app_model.mode = AppMode.AGENT
mock_api_token = Mock()
mock_api_token.app_id = mock_app_model.id
mock_api_token.tenant_id = mock_app_model.tenant_id
mock_validate_token.return_value = mock_api_token
mock_tenant = Mock()
mock_tenant.status = TenantStatus.NORMAL
mock_db.session.get.side_effect = [mock_app_model, mock_tenant]
setup_mock_tenant_owner_execute_result(mock_db, mock_tenant, Mock(current_tenant=mock_tenant))
with app.test_request_context("/parameters", method="GET", headers={"Authorization": "Bearer test_token"}):
with pytest.raises(AgentNotPublishedError):
AppParameterApi().get()
@patch("controllers.service_api.wraps.user_logged_in")
@patch("controllers.service_api.wraps.current_app")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@@ -31,10 +31,12 @@ from controllers.service_api.app.completion import (
CompletionStopApi,
)
from controllers.service_api.app.error import (
AgentNotPublishedError,
AppUnavailableError,
ConversationCompletedError,
NotChatAppError,
)
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
from core.errors.error import QuotaExceededError
from graphon.model_runtime.errors.invoke import InvokeError
from models.model import App, AppMode, EndUser
@@ -516,6 +518,22 @@ class TestChatApiController:
with pytest.raises(BadRequest):
handler(api, session=Mock(), app_model=app_model, end_user=end_user)
def test_agent_not_published_error_mapped(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
AppGenerateService,
"generate",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AgentAppNotPublishedError("Agent has not been published")),
)
api = ChatApi()
handler = unwrap(api.post)
app_model = SimpleNamespace(mode=AppMode.AGENT.value)
end_user = SimpleNamespace()
with app.test_request_context("/chat-messages", method="POST", json={"inputs": {}, "query": "hi"}):
with pytest.raises(AgentNotPublishedError):
handler(api, session=Mock(), app_model=app_model, end_user=end_user)
class TestChatStopApiController:
def test_wrong_mode(self, app: Flask) -> None:
@@ -9,7 +9,8 @@ import pytest
from flask import Flask
from controllers.web.app import AppAccessMode, AppMeta, AppParameterApi, AppWebAuthPermission
from controllers.web.error import AppUnavailableError
from controllers.web.error import AgentNotPublishedError, AppUnavailableError
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
# ---------------------------------------------------------------------------
@@ -80,6 +81,18 @@ class TestAppParameterApi:
with pytest.raises(AppUnavailableError):
AppParameterApi().get(app_model, SimpleNamespace())
def test_agent_mode_unpublished_raises_friendly_error(self, app: Flask) -> None:
app_model = SimpleNamespace(mode="agent")
with (
app.test_request_context("/parameters"),
patch(
"controllers.web.app.get_published_agent_app_feature_dict_and_user_input_form",
side_effect=AgentAppNotPublishedError("Agent has not been published"),
),
):
with pytest.raises(AgentNotPublishedError):
AppParameterApi().get(app_model, SimpleNamespace())
# ---------------------------------------------------------------------------
# AppMeta
@@ -10,6 +10,7 @@ from flask import Flask
from controllers.web.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi
from controllers.web.error import (
AgentNotPublishedError,
CompletionRequestError,
NotChatAppError,
NotCompletionAppError,
@@ -17,6 +18,7 @@ from controllers.web.error import (
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
from core.app.apps.agent_app.errors import AgentAppNotPublishedError
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from graphon.model_runtime.errors.invoke import InvokeError
@@ -142,6 +144,19 @@ class TestChatApi:
with pytest.raises(CompletionRequestError):
ChatApi().post(_chat_app(), _end_user())
@patch(
"controllers.web.completion.AppGenerateService.generate",
side_effect=AgentAppNotPublishedError("Agent has not been published"),
)
@patch("controllers.web.completion.web_ns")
def test_agent_not_published_error_mapped(self, mock_ns: MagicMock, mock_gen: MagicMock, app: Flask) -> None:
mock_ns.payload = {"inputs": {}, "query": "x"}
app_model = SimpleNamespace(id="app-1", mode="agent")
with app.test_request_context("/chat-messages", method="POST"):
with pytest.raises(AgentNotPublishedError):
ChatApi().post(app_model, _end_user())
# ---------------------------------------------------------------------------
# ChatStopApi
@@ -6,6 +6,7 @@ import pytest
from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.web.error import (
AgentNotPublishedError,
AppMoreLikeThisDisabledError,
AppSuggestedQuestionsAfterAnswerDisabledError,
AppUnavailableError,
@@ -29,6 +30,7 @@ from controllers.web.error import (
_ERROR_SPECS: list[tuple[type, str, int]] = [
(AppUnavailableError, "app_unavailable", 400),
(AgentNotPublishedError, "agent_not_published", 400),
(NotCompletionAppError, "not_completion_app", 400),
(NotChatAppError, "not_chat_app", 400),
(NotWorkflowAppError, "not_workflow_app", 400),