mirror of
https://github.com/langgenius/dify.git
synced 2026-06-18 07:41:09 +08:00
fix(agent): add agent app duplicate endpoint (#37571)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
872b5a081f
commit
912c0fa8d1
@ -12,6 +12,7 @@ from controllers.console.app.app import (
|
||||
AppListQuery,
|
||||
AppPagination,
|
||||
AppPartial,
|
||||
CopyAppPayload,
|
||||
UpdateAppPayload,
|
||||
_normalize_app_list_query_args,
|
||||
)
|
||||
@ -134,6 +135,7 @@ register_schema_models(
|
||||
console_ns,
|
||||
AgentAppCreatePayload,
|
||||
AgentAppUpdatePayload,
|
||||
CopyAppPayload,
|
||||
AgentInviteOptionsQuery,
|
||||
AgentLogsQuery,
|
||||
AgentStatisticsQuery,
|
||||
@ -348,6 +350,34 @@ class AgentAppApi(Resource):
|
||||
return "", 204
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/copy")
|
||||
class AgentAppCopyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[CopyAppPayload.__name__])
|
||||
@console_ns.response(201, "Agent app copied successfully", console_ns.models[AppDetailWithSite.__name__])
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@console_ns.response(400, "Invalid request parameters")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("apps")
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
args = CopyAppPayload.model_validate(console_ns.payload or {})
|
||||
copied_app = _agent_roster_service().duplicate_agent_app(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=str(agent_id),
|
||||
account=current_user,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
)
|
||||
return _serialize_agent_app_detail(copied_app), 201
|
||||
|
||||
|
||||
@console_ns.route("/agent/invite-options")
|
||||
class AgentInviteOptionsApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(AgentInviteOptionsQuery))
|
||||
|
||||
@ -508,6 +508,27 @@ Stop a running Agent App chat message generation
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent app composer validation result | **application/json**: [AgentComposerValidateResponse](#agentcomposervalidateresponse)<br> |
|
||||
|
||||
### [POST] /agent/{agent_id}/copy
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [CopyAppPayload](#copyapppayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Agent app copied successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
|
||||
| 400 | Invalid request parameters | |
|
||||
| 403 | Insufficient permissions | |
|
||||
|
||||
### [GET] /agent/{agent_id}/drive/files
|
||||
List agent drive entries for an Agent App
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ from models.agent import (
|
||||
)
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
from models.enums import AppStatus
|
||||
from models.model import App, AppMode
|
||||
from models.model import App, AppMode, IconType
|
||||
from models.workflow import Workflow
|
||||
from services.agent.agent_soul_state import agent_soul_has_model
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
@ -29,7 +29,10 @@ from services.agent.errors import (
|
||||
AgentNotFoundError,
|
||||
AgentVersionNotFoundError,
|
||||
)
|
||||
from services.app_service import AppService, CreateAppParams
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
class AgentReferencingWorkflow(TypedDict):
|
||||
@ -48,6 +51,28 @@ class AgentReferencingWorkflow(TypedDict):
|
||||
|
||||
|
||||
class AgentRosterService:
|
||||
_APP_MODEL_CONFIG_COPY_FIELDS = (
|
||||
"opening_statement",
|
||||
"suggested_questions",
|
||||
"suggested_questions_after_answer",
|
||||
"speech_to_text",
|
||||
"text_to_speech",
|
||||
"more_like_this",
|
||||
"model",
|
||||
"user_input_form",
|
||||
"dataset_query_variable",
|
||||
"pre_prompt",
|
||||
"agent_mode",
|
||||
"sensitive_word_avoidance",
|
||||
"retriever_resource",
|
||||
"prompt_type",
|
||||
"chat_prompt_config",
|
||||
"completion_prompt_config",
|
||||
"dataset_configs",
|
||||
"external_data_tools",
|
||||
"file_upload",
|
||||
)
|
||||
|
||||
def __init__(self, session: Any):
|
||||
self._session = session
|
||||
|
||||
@ -418,6 +443,142 @@ class AgentRosterService:
|
||||
raise AgentNotFoundError()
|
||||
return app
|
||||
|
||||
def duplicate_agent_app(
|
||||
self,
|
||||
*,
|
||||
tenant_id: str,
|
||||
agent_id: str,
|
||||
account: Any,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
icon_type: Any = None,
|
||||
icon: str | None = None,
|
||||
icon_background: str | None = None,
|
||||
) -> App:
|
||||
source_app = self.get_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
source_agent = self.get_app_backing_agent(tenant_id=tenant_id, app_id=source_app.id)
|
||||
if source_agent is None:
|
||||
raise AgentNotFoundError()
|
||||
|
||||
copied_name = name or self._next_duplicate_agent_name(tenant_id=tenant_id, base_name=source_app.name)
|
||||
copied_description = description if description is not None else source_app.description
|
||||
copied_icon_type = icon_type if icon_type is not None else source_app.icon_type
|
||||
copied_icon = icon if icon is not None else source_app.icon
|
||||
copied_icon_background = icon_background if icon_background is not None else source_app.icon_background
|
||||
|
||||
target_app = AppService().create_app(
|
||||
tenant_id,
|
||||
CreateAppParams(
|
||||
name=copied_name,
|
||||
description=copied_description,
|
||||
mode="agent",
|
||||
agent_role=source_agent.role or "",
|
||||
icon_type=self._normalize_app_icon_type(copied_icon_type),
|
||||
icon=copied_icon,
|
||||
icon_background=copied_icon_background,
|
||||
api_rph=source_app.api_rph or 0,
|
||||
api_rpm=source_app.api_rpm or 0,
|
||||
max_active_requests=source_app.max_active_requests,
|
||||
),
|
||||
account,
|
||||
)
|
||||
|
||||
target_app.enable_site = source_app.enable_site
|
||||
target_app.enable_api = source_app.enable_api
|
||||
target_app.use_icon_as_answer_icon = source_app.use_icon_as_answer_icon
|
||||
target_app.tracing = source_app.tracing
|
||||
|
||||
self._copy_app_model_config(source_app=source_app, target_app=target_app, account_id=account.id)
|
||||
self._copy_agent_active_snapshot(
|
||||
tenant_id=tenant_id,
|
||||
source_agent=source_agent,
|
||||
target_app_id=target_app.id,
|
||||
account_id=account.id,
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
try:
|
||||
original_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(source_app.id)
|
||||
access_mode = original_settings.access_mode
|
||||
except Exception:
|
||||
access_mode = "public"
|
||||
EnterpriseService.WebAppAuth.update_app_access_mode(target_app.id, access_mode)
|
||||
|
||||
return target_app
|
||||
|
||||
@staticmethod
|
||||
def _normalize_app_icon_type(icon_type: IconType | str | None) -> str | None:
|
||||
if icon_type is None:
|
||||
return None
|
||||
if isinstance(icon_type, IconType):
|
||||
return icon_type.value
|
||||
return icon_type
|
||||
|
||||
def _copy_app_model_config(self, *, source_app: App, target_app: App, account_id: str) -> None:
|
||||
source_config = source_app.app_model_config
|
||||
target_config = target_app.app_model_config
|
||||
if source_config is None or target_config is None:
|
||||
return
|
||||
|
||||
for field_name in self._APP_MODEL_CONFIG_COPY_FIELDS:
|
||||
setattr(target_config, field_name, getattr(source_config, field_name))
|
||||
target_config.updated_by = account_id
|
||||
|
||||
def _copy_agent_active_snapshot(
|
||||
self,
|
||||
*,
|
||||
tenant_id: str,
|
||||
source_agent: Agent,
|
||||
target_app_id: str,
|
||||
account_id: str,
|
||||
) -> None:
|
||||
target_agent = self.get_app_backing_agent(tenant_id=tenant_id, app_id=target_app_id)
|
||||
if target_agent is None:
|
||||
raise AgentNotFoundError()
|
||||
|
||||
source_version = self._get_version(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=source_agent.id,
|
||||
version_id=source_agent.active_config_snapshot_id,
|
||||
)
|
||||
target_version = self._get_version(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=target_agent.id,
|
||||
version_id=target_agent.active_config_snapshot_id,
|
||||
)
|
||||
|
||||
target_version.config_snapshot = AgentSoulConfig.model_validate(source_version.config_snapshot_dict)
|
||||
target_version.summary = source_version.summary
|
||||
target_version.version_note = source_version.version_note
|
||||
target_version.created_by = account_id
|
||||
target_agent.active_config_has_model = agent_soul_has_model(target_version.config_snapshot)
|
||||
target_agent.updated_by = account_id
|
||||
|
||||
def _next_duplicate_agent_name(self, *, tenant_id: str, base_name: str) -> str:
|
||||
suffix = " copy"
|
||||
max_base_len = 255 - len(suffix)
|
||||
first_candidate = f"{base_name[:max_base_len]}{suffix}"
|
||||
candidates = [first_candidate]
|
||||
for index in range(2, 100):
|
||||
numbered_suffix = f" copy {index}"
|
||||
candidates.append(f"{base_name[: 255 - len(numbered_suffix)]}{numbered_suffix}")
|
||||
|
||||
existing_names = set(
|
||||
self._session.scalars(
|
||||
select(Agent.name).where(
|
||||
Agent.tenant_id == tenant_id,
|
||||
Agent.scope == AgentScope.ROSTER,
|
||||
Agent.status == AgentStatus.ACTIVE,
|
||||
Agent.name.in_(candidates),
|
||||
)
|
||||
).all()
|
||||
)
|
||||
for candidate in candidates:
|
||||
if candidate not in existing_names:
|
||||
return candidate
|
||||
return f"{base_name[:245]} copy {int(naive_utc_now().timestamp())}"
|
||||
|
||||
def list_workflows_referencing_app_agent(self, *, tenant_id: str, app_id: str) -> list[AgentReferencingWorkflow]:
|
||||
"""List the workflow apps that reference this Agent App's bound Agent.
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ from controllers.console.agent.composer import (
|
||||
)
|
||||
from controllers.console.agent.roster import (
|
||||
AgentAppApi,
|
||||
AgentAppCopyApi,
|
||||
AgentAppListApi,
|
||||
AgentInviteOptionsApi,
|
||||
AgentLogsApi,
|
||||
@ -140,6 +141,7 @@ def test_agent_v2_console_routes_are_agent_id_first() -> None:
|
||||
"/agent/<uuid:agent_id>/composer/validate",
|
||||
"/agent/<uuid:agent_id>/composer/candidates",
|
||||
"/agent/<uuid:agent_id>/features",
|
||||
"/agent/<uuid:agent_id>/copy",
|
||||
"/agent/<uuid:agent_id>/referencing-workflows",
|
||||
"/agent/<uuid:agent_id>/drive/files",
|
||||
"/agent/<uuid:agent_id>/sandbox/files",
|
||||
@ -347,6 +349,52 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
assert captured["delete"] is app_model
|
||||
|
||||
|
||||
def test_agent_app_copy_uses_agent_id_and_returns_agent_detail(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
|
||||
) -> None:
|
||||
agent_id = "00000000-0000-0000-0000-000000000001"
|
||||
current_user = SimpleNamespace(id=account_id)
|
||||
copied_app = _app_detail_obj(id="copied-app", bound_agent_id="copied-agent")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class FakeRosterService:
|
||||
def duplicate_agent_app(self, **kwargs: object) -> object:
|
||||
captured.update(kwargs)
|
||||
return copied_app
|
||||
|
||||
monkeypatch.setattr(roster_controller, "_agent_roster_service", lambda: FakeRosterService())
|
||||
monkeypatch.setattr(
|
||||
roster_controller,
|
||||
"_serialize_agent_app_detail",
|
||||
lambda app_model: {"id": "copied-agent", "app_id": app_model.id, "name": app_model.name},
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
"/console/api/agent/00000000-0000-0000-0000-000000000001/copy",
|
||||
json={
|
||||
"name": "Iris copy",
|
||||
"description": "Copied",
|
||||
"icon_type": "emoji",
|
||||
"icon": "sparkles",
|
||||
"icon_background": "#fff",
|
||||
},
|
||||
):
|
||||
copied, status = unwrap(AgentAppCopyApi.post)(AgentAppCopyApi(), "tenant-1", current_user, agent_id)
|
||||
|
||||
assert status == 201
|
||||
assert copied == {"id": "copied-agent", "app_id": "copied-app", "name": "Iris"}
|
||||
assert captured == {
|
||||
"tenant_id": "tenant-1",
|
||||
"agent_id": agent_id,
|
||||
"account": current_user,
|
||||
"name": "Iris copy",
|
||||
"description": "Copied",
|
||||
"icon_type": "emoji",
|
||||
"icon": "sparkles",
|
||||
"icon_background": "#fff",
|
||||
}
|
||||
|
||||
|
||||
def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ from models.agent_config_entities import (
|
||||
DeclaredOutputType,
|
||||
WorkflowNodeJobConfig,
|
||||
)
|
||||
from models.model import IconType
|
||||
from models.workflow import Workflow
|
||||
from services.agent import composer_service, roster_service
|
||||
from services.agent.agent_soul_state import agent_soul_has_model
|
||||
@ -1309,6 +1310,267 @@ class TestAgentAppBackingAgent:
|
||||
with pytest.raises(roster_service.AgentNotFoundError):
|
||||
service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-x")
|
||||
|
||||
def test_duplicate_agent_app_copies_app_config_and_active_soul(self, monkeypatch):
|
||||
source_config = SimpleNamespace(
|
||||
opening_statement="hello",
|
||||
suggested_questions='["q1"]',
|
||||
suggested_questions_after_answer='{"enabled": true}',
|
||||
speech_to_text='{"enabled": false}',
|
||||
text_to_speech='{"enabled": false}',
|
||||
more_like_this='{"enabled": false}',
|
||||
model=None,
|
||||
user_input_form=None,
|
||||
dataset_query_variable=None,
|
||||
pre_prompt=None,
|
||||
agent_mode=None,
|
||||
sensitive_word_avoidance=None,
|
||||
retriever_resource='{"enabled": true}',
|
||||
prompt_type="simple",
|
||||
chat_prompt_config=None,
|
||||
completion_prompt_config=None,
|
||||
dataset_configs=None,
|
||||
external_data_tools=None,
|
||||
file_upload='{"image": {"enabled": true}}',
|
||||
)
|
||||
target_config = SimpleNamespace(**dict.fromkeys(AgentRosterService._APP_MODEL_CONFIG_COPY_FIELDS))
|
||||
source_app = SimpleNamespace(
|
||||
id="source-app",
|
||||
tenant_id="tenant-1",
|
||||
name="Iris",
|
||||
description="source desc",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
api_rph=1,
|
||||
api_rpm=2,
|
||||
max_active_requests=3,
|
||||
enable_site=False,
|
||||
enable_api=True,
|
||||
use_icon_as_answer_icon=True,
|
||||
tracing="{}",
|
||||
app_model_config=source_config,
|
||||
)
|
||||
target_app = SimpleNamespace(
|
||||
id="target-app",
|
||||
app_model_config=target_config,
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
tracing=None,
|
||||
)
|
||||
source_agent = Agent(
|
||||
id="source-agent",
|
||||
tenant_id="tenant-1",
|
||||
name="Iris",
|
||||
description="source desc",
|
||||
role="Analyst",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
app_id="source-app",
|
||||
active_config_snapshot_id="source-version",
|
||||
active_config_has_model=True,
|
||||
)
|
||||
target_agent = Agent(
|
||||
id="target-agent",
|
||||
tenant_id="tenant-1",
|
||||
name="Iris copy",
|
||||
description="source desc",
|
||||
role="Analyst",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
app_id="target-app",
|
||||
active_config_snapshot_id="target-version",
|
||||
)
|
||||
source_version = AgentConfigSnapshot(
|
||||
id="source-version",
|
||||
tenant_id="tenant-1",
|
||||
agent_id="source-agent",
|
||||
version=1,
|
||||
config_snapshot=_agent_soul_with_model(),
|
||||
summary="configured",
|
||||
version_note="v1",
|
||||
created_by="account-1",
|
||||
)
|
||||
target_version = AgentConfigSnapshot(
|
||||
id="target-version",
|
||||
tenant_id="tenant-1",
|
||||
agent_id="target-agent",
|
||||
version=1,
|
||||
config_snapshot=AgentSoulConfig(),
|
||||
created_by="account-1",
|
||||
)
|
||||
session = FakeSession(
|
||||
scalar=[source_agent, source_app, source_agent, target_agent, source_version, target_version],
|
||||
scalars=[[]],
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id: str, params, account: object) -> object:
|
||||
captured["tenant_id"] = tenant_id
|
||||
captured["params"] = params
|
||||
captured["account"] = account
|
||||
return target_app
|
||||
|
||||
monkeypatch.setattr(roster_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(
|
||||
roster_service.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
|
||||
account = SimpleNamespace(id="account-1")
|
||||
duplicated = AgentRosterService(session).duplicate_agent_app(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="source-agent",
|
||||
account=account,
|
||||
)
|
||||
|
||||
assert duplicated is target_app
|
||||
params = captured["params"]
|
||||
assert params.name == "Iris copy"
|
||||
assert params.mode == "agent"
|
||||
assert params.agent_role == "Analyst"
|
||||
assert target_app.enable_site is False
|
||||
assert target_app.enable_api is True
|
||||
assert target_app.use_icon_as_answer_icon is True
|
||||
assert target_app.tracing == "{}"
|
||||
assert target_config.opening_statement == "hello"
|
||||
assert target_config.file_upload == '{"image": {"enabled": true}}'
|
||||
assert target_config.updated_by == "account-1"
|
||||
assert target_version.config_snapshot.model.model == "gpt-4o"
|
||||
assert target_version.summary == "configured"
|
||||
assert target_version.version_note == "v1"
|
||||
assert target_agent.active_config_has_model is True
|
||||
assert target_agent.updated_by == "account-1"
|
||||
assert session.commits == 1
|
||||
|
||||
def test_duplicate_agent_app_inherits_webapp_access_mode(self, monkeypatch):
|
||||
source_app = SimpleNamespace(
|
||||
id="source-app",
|
||||
tenant_id="tenant-1",
|
||||
name="Iris",
|
||||
description="source desc",
|
||||
icon_type=None,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
api_rph=1,
|
||||
api_rpm=2,
|
||||
max_active_requests=3,
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
tracing=None,
|
||||
)
|
||||
source_agent = SimpleNamespace(id="source-agent", role="Analyst")
|
||||
target_app = SimpleNamespace(id="target-app")
|
||||
session = FakeSession()
|
||||
service = AgentRosterService(session)
|
||||
monkeypatch.setattr(service, "get_agent_app_model", lambda **_: source_app)
|
||||
monkeypatch.setattr(service, "get_app_backing_agent", lambda **_: source_agent)
|
||||
monkeypatch.setattr(service, "_copy_app_model_config", lambda **_: None)
|
||||
monkeypatch.setattr(service, "_copy_agent_active_snapshot", lambda **_: None)
|
||||
monkeypatch.setattr(service, "_next_duplicate_agent_name", lambda **_: "Iris copy")
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id: str, params, account: object) -> object:
|
||||
return target_app
|
||||
|
||||
access_mode_updates = []
|
||||
|
||||
class FakeWebAppAuth:
|
||||
@classmethod
|
||||
def get_app_access_mode_by_id(cls, app_id: str) -> object:
|
||||
return SimpleNamespace(access_mode="private")
|
||||
|
||||
@classmethod
|
||||
def update_app_access_mode(cls, app_id: str, access_mode: str) -> None:
|
||||
access_mode_updates.append((app_id, access_mode))
|
||||
|
||||
monkeypatch.setattr(roster_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(
|
||||
roster_service.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=True)),
|
||||
)
|
||||
monkeypatch.setattr(roster_service.EnterpriseService, "WebAppAuth", FakeWebAppAuth)
|
||||
|
||||
duplicated = service.duplicate_agent_app(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="source-agent",
|
||||
account=SimpleNamespace(id="account-1"),
|
||||
)
|
||||
|
||||
assert duplicated is target_app
|
||||
assert access_mode_updates == [("target-app", "private")]
|
||||
|
||||
def test_duplicate_agent_app_falls_back_to_public_access_mode(self, monkeypatch):
|
||||
source_app = SimpleNamespace(
|
||||
id="source-app",
|
||||
tenant_id="tenant-1",
|
||||
name="Iris",
|
||||
description="source desc",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
api_rph=1,
|
||||
api_rpm=2,
|
||||
max_active_requests=3,
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
tracing=None,
|
||||
)
|
||||
source_agent = SimpleNamespace(id="source-agent", role="Analyst")
|
||||
target_app = SimpleNamespace(id="target-app")
|
||||
session = FakeSession()
|
||||
service = AgentRosterService(session)
|
||||
monkeypatch.setattr(service, "get_agent_app_model", lambda **_: source_app)
|
||||
monkeypatch.setattr(service, "get_app_backing_agent", lambda **_: source_agent)
|
||||
monkeypatch.setattr(service, "_copy_app_model_config", lambda **_: None)
|
||||
monkeypatch.setattr(service, "_copy_agent_active_snapshot", lambda **_: None)
|
||||
monkeypatch.setattr(service, "_next_duplicate_agent_name", lambda **_: "Iris copy")
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id: str, params, account: object) -> object:
|
||||
return target_app
|
||||
|
||||
access_mode_updates = []
|
||||
|
||||
class FakeWebAppAuth:
|
||||
@classmethod
|
||||
def get_app_access_mode_by_id(cls, app_id: str) -> object:
|
||||
raise ValueError("not found")
|
||||
|
||||
@classmethod
|
||||
def update_app_access_mode(cls, app_id: str, access_mode: str) -> None:
|
||||
access_mode_updates.append((app_id, access_mode))
|
||||
|
||||
monkeypatch.setattr(roster_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(
|
||||
roster_service.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=True)),
|
||||
)
|
||||
monkeypatch.setattr(roster_service.EnterpriseService, "WebAppAuth", FakeWebAppAuth)
|
||||
|
||||
service.duplicate_agent_app(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="source-agent",
|
||||
account=SimpleNamespace(id="account-1"),
|
||||
)
|
||||
|
||||
assert access_mode_updates == [("target-app", "public")]
|
||||
|
||||
def test_normalize_app_icon_type(self):
|
||||
assert AgentRosterService._normalize_app_icon_type(None) is None
|
||||
assert AgentRosterService._normalize_app_icon_type(IconType.EMOJI) == "emoji"
|
||||
assert AgentRosterService._normalize_app_icon_type("image") == "image"
|
||||
|
||||
|
||||
class TestListWorkflowsReferencingAppAgent:
|
||||
def test_groups_bindings_by_workflow_app_and_sorts_by_name(self):
|
||||
|
||||
@ -61,6 +61,9 @@ import {
|
||||
zPostAgentByAgentIdComposerValidateBody,
|
||||
zPostAgentByAgentIdComposerValidatePath,
|
||||
zPostAgentByAgentIdComposerValidateResponse,
|
||||
zPostAgentByAgentIdCopyBody,
|
||||
zPostAgentByAgentIdCopyPath,
|
||||
zPostAgentByAgentIdCopyResponse,
|
||||
zPostAgentByAgentIdFeaturesBody,
|
||||
zPostAgentByAgentIdFeaturesPath,
|
||||
zPostAgentByAgentIdFeaturesResponse,
|
||||
@ -239,6 +242,22 @@ export const composer = {
|
||||
validate,
|
||||
}
|
||||
|
||||
export const post3 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAgentByAgentIdCopy',
|
||||
path: '/agent/{agent_id}/copy',
|
||||
successStatus: 201,
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ body: zPostAgentByAgentIdCopyBody, params: zPostAgentByAgentIdCopyPath }))
|
||||
.output(zPostAgentByAgentIdCopyResponse)
|
||||
|
||||
export const copy = {
|
||||
post: post3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Time-limited external signed URL for one Agent App drive value
|
||||
*/
|
||||
@ -320,7 +339,7 @@ export const drive = {
|
||||
/**
|
||||
* Update an Agent App's presentation features (opener, follow-up, citations, ...)
|
||||
*/
|
||||
export const post3 = oc
|
||||
export const post4 = oc
|
||||
.route({
|
||||
description: 'Update an Agent App\'s presentation features (opener, follow-up, citations, ...)',
|
||||
inputStructure: 'detailed',
|
||||
@ -335,13 +354,13 @@ export const post3 = oc
|
||||
.output(zPostAgentByAgentIdFeaturesResponse)
|
||||
|
||||
export const features = {
|
||||
post: post3,
|
||||
post: post4,
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update Agent App message feedback
|
||||
*/
|
||||
export const post4 = oc
|
||||
export const post5 = oc
|
||||
.route({
|
||||
description: 'Create or update Agent App message feedback',
|
||||
inputStructure: 'detailed',
|
||||
@ -356,7 +375,7 @@ export const post4 = oc
|
||||
.output(zPostAgentByAgentIdFeedbacksResponse)
|
||||
|
||||
export const feedbacks = {
|
||||
post: post4,
|
||||
post: post5,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -379,7 +398,7 @@ export const delete_ = oc
|
||||
/**
|
||||
* Commit an uploaded file into the Agent App drive under files/<name>
|
||||
*/
|
||||
export const post5 = oc
|
||||
export const post6 = oc
|
||||
.route({
|
||||
description: 'Commit an uploaded file into the Agent App drive under files/<name>',
|
||||
inputStructure: 'detailed',
|
||||
@ -394,7 +413,7 @@ export const post5 = oc
|
||||
|
||||
export const files2 = {
|
||||
delete: delete_,
|
||||
post: post5,
|
||||
post: post6,
|
||||
}
|
||||
|
||||
export const get9 = oc
|
||||
@ -483,7 +502,7 @@ export const read = {
|
||||
/**
|
||||
* Upload one Agent App sandbox file as a Dify ToolFile mapping
|
||||
*/
|
||||
export const post6 = oc
|
||||
export const post7 = oc
|
||||
.route({
|
||||
description: 'Upload one Agent App sandbox file as a Dify ToolFile mapping',
|
||||
inputStructure: 'detailed',
|
||||
@ -501,7 +520,7 @@ export const post6 = oc
|
||||
.output(zPostAgentByAgentIdSandboxFilesUploadResponse)
|
||||
|
||||
export const upload = {
|
||||
post: post6,
|
||||
post: post7,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -537,7 +556,7 @@ export const sandbox = {
|
||||
/**
|
||||
* Validate + standardize a Skill into an Agent App drive
|
||||
*/
|
||||
export const post7 = oc
|
||||
export const post8 = oc
|
||||
.route({
|
||||
description: 'Validate + standardize a Skill into an Agent App drive',
|
||||
inputStructure: 'detailed',
|
||||
@ -551,13 +570,13 @@ export const post7 = oc
|
||||
.output(zPostAgentByAgentIdSkillsStandardizeResponse)
|
||||
|
||||
export const standardize = {
|
||||
post: post7,
|
||||
post: post8,
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload + validate a Skill package for an Agent App
|
||||
*/
|
||||
export const post8 = oc
|
||||
export const post9 = oc
|
||||
.route({
|
||||
description: 'Upload + validate a Skill package for an Agent App',
|
||||
inputStructure: 'detailed',
|
||||
@ -571,13 +590,13 @@ export const post8 = oc
|
||||
.output(zPostAgentByAgentIdSkillsUploadResponse)
|
||||
|
||||
export const upload2 = {
|
||||
post: post8,
|
||||
post: post9,
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer CLI tool + ENV suggestions from a standardized Agent App skill
|
||||
*/
|
||||
export const post9 = oc
|
||||
export const post10 = oc
|
||||
.route({
|
||||
description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill',
|
||||
inputStructure: 'detailed',
|
||||
@ -590,7 +609,7 @@ export const post9 = oc
|
||||
.output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse)
|
||||
|
||||
export const inferTools = {
|
||||
post: post9,
|
||||
post: post10,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -714,6 +733,7 @@ export const byAgentId = {
|
||||
put: put2,
|
||||
chatMessages,
|
||||
composer,
|
||||
copy,
|
||||
drive,
|
||||
features,
|
||||
feedbacks,
|
||||
@ -738,7 +758,7 @@ export const get18 = oc
|
||||
.input(z.object({ query: zGetAgentQuery.optional() }))
|
||||
.output(zGetAgentResponse)
|
||||
|
||||
export const post10 = oc
|
||||
export const post11 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -752,7 +772,7 @@ export const post10 = oc
|
||||
|
||||
export const agent = {
|
||||
get: get18,
|
||||
post: post10,
|
||||
post: post11,
|
||||
inviteOptions,
|
||||
byAgentId,
|
||||
}
|
||||
|
||||
@ -122,6 +122,14 @@ export type AgentComposerValidateResponse = {
|
||||
warnings?: Array<ComposerValidationWarningResponse>
|
||||
}
|
||||
|
||||
export type CopyAppPayload = {
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: IconType | null
|
||||
name?: string | null
|
||||
}
|
||||
|
||||
export type AgentDriveListResponse = {
|
||||
items?: Array<AgentDriveItemResponse>
|
||||
}
|
||||
@ -1703,6 +1711,27 @@ export type PostAgentByAgentIdComposerValidateResponses = {
|
||||
export type PostAgentByAgentIdComposerValidateResponse
|
||||
= PostAgentByAgentIdComposerValidateResponses[keyof PostAgentByAgentIdComposerValidateResponses]
|
||||
|
||||
export type PostAgentByAgentIdCopyData = {
|
||||
body: CopyAppPayload
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/agent/{agent_id}/copy'
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdCopyErrors = {
|
||||
400: unknown
|
||||
403: unknown
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdCopyResponses = {
|
||||
201: AppDetailWithSite
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdCopyResponse
|
||||
= PostAgentByAgentIdCopyResponses[keyof PostAgentByAgentIdCopyResponses]
|
||||
|
||||
export type GetAgentByAgentIdDriveFilesData = {
|
||||
body?: never
|
||||
path: {
|
||||
|
||||
@ -109,6 +109,17 @@ export const zAgentAppUpdatePayload = z.object({
|
||||
use_icon_as_answer_icon: z.boolean().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* CopyAppPayload
|
||||
*/
|
||||
export const zCopyAppPayload = z.object({
|
||||
description: z.string().max(400).nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: zIconType.nullish(),
|
||||
name: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DeletedTool
|
||||
*/
|
||||
@ -2180,6 +2191,17 @@ export const zPostAgentByAgentIdComposerValidatePath = z.object({
|
||||
*/
|
||||
export const zPostAgentByAgentIdComposerValidateResponse = zAgentComposerValidateResponse
|
||||
|
||||
export const zPostAgentByAgentIdCopyBody = zCopyAppPayload
|
||||
|
||||
export const zPostAgentByAgentIdCopyPath = z.object({
|
||||
agent_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Agent app copied successfully
|
||||
*/
|
||||
export const zPostAgentByAgentIdCopyResponse = zAppDetailWithSite
|
||||
|
||||
export const zGetAgentByAgentIdDriveFilesPath = z.object({
|
||||
agent_id: z.string(),
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user