diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 60fb018915b..ea313ff37aa 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -257,7 +257,6 @@ class AgentAppDetailWithSite(GenericAppDetailWithSite): debug_conversation_has_messages: bool = False debug_conversation_message_count: int = 0 role: str | None = None - active_config_is_published: bool = False class AgentDebugConversationRefreshResponse(BaseModel): @@ -410,10 +409,6 @@ def _serialize_agent_app_detail( payload["debug_conversation_has_messages"] = message_count > 0 payload["debug_conversation_message_count"] = message_count payload["role"] = agent.role or "" - payload["active_config_is_published"] = roster_service.active_config_is_published( - tenant_id=app_model.tenant_id, - agent=agent, - ) return payload diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index 50105273e1c..91e22eb5b36 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -383,6 +383,7 @@ class AgentAppComposerResponse(ResponseModel): variant: Literal[ComposerVariant.AGENT_APP] agent: AgentComposerAgentResponse active_config_snapshot: AgentConfigSnapshotSummaryResponse | None = None + active_config_is_published: bool draft: AgentConfigDraftSummaryResponse | None = None agent_soul: AgentSoulConfig save_options: list[ComposerSaveStrategy] diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 367ba473e9e..34478b71b7c 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -13243,6 +13243,7 @@ Model class for AI model. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| active_config_is_published | boolean | | Yes | | active_config_snapshot | [AgentConfigSnapshotSummaryResponse](#agentconfigsnapshotsummaryresponse) | | No | | agent | [AgentComposerAgentResponse](#agentcomposeragentresponse) | | Yes | | agent_soul | [AgentSoulConfig](#agentsoulconfig) | | Yes | @@ -13282,7 +13283,6 @@ Model class for AI model. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | -| active_config_is_published | boolean | | No | | api_base_url | string | | No | | app_id | string | | No | | backing_app_id | string | | No | diff --git a/api/services/agent/composer_service.py b/api/services/agent/composer_service.py index 96f0b8dde23..aba9e9d8fc8 100644 --- a/api/services/agent/composer_service.py +++ b/api/services/agent/composer_service.py @@ -405,6 +405,7 @@ class AgentComposerService: "variant": ComposerVariant.AGENT_APP.value, "agent": cls._serialize_agent(agent), "active_config_snapshot": cls._serialize_version(version), + "active_config_is_published": bool(agent.active_config_snapshot_id and agent.active_config_is_published), "draft": cls._serialize_draft(draft), "agent_soul": draft.config_snapshot_dict, "save_options": [ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value], diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 14628b2614d..60c33073408 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -115,6 +115,7 @@ def _agent_app_composer_response() -> dict: "active_config_snapshot_id": "version-1", }, "active_config_snapshot": _version_response(), + "active_config_is_published": True, "agent_soul": {}, "save_options": ["save_to_current_version"], } @@ -376,7 +377,7 @@ def test_agent_app_list_and_create_use_agent_route( assert created["app_id"] == "app-created" assert created["debug_conversation_id"] == "debug-conversation-created" assert created["role"] == "Created role" - assert created["active_config_is_published"] is False + assert "active_config_is_published" not in created assert "bound_agent_id" not in created create_call = cast(dict[str, object], captured["create"]) create_params = cast(Any, create_call["params"]) @@ -487,7 +488,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert detail["debug_conversation_has_messages"] is True assert detail["debug_conversation_message_count"] == 2 assert detail["role"] == "Resolved role" - assert detail["active_config_is_published"] is False + assert "active_config_is_published" not in detail assert "bound_agent_id" not in detail assert captured["get_app"] == {"app": app_model, "session": session} with app.test_request_context( @@ -502,7 +503,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert updated["debug_conversation_has_messages"] is True assert updated["debug_conversation_message_count"] == 2 assert updated["role"] == "Resolved role" - assert updated["active_config_is_published"] is False + assert "active_config_is_published" not in updated assert "bound_agent_id" not in updated update_call = cast(dict[str, object], captured["update"]) assert update_call["app"] is app_model @@ -845,9 +846,6 @@ def test_agent_app_update_allows_empty_role(app: Flask, monkeypatch: pytest.Monk monkeypatch.setattr( roster_controller.AgentRosterService, "count_agent_app_debug_conversation_messages", lambda _self, **kwargs: 0 ) - monkeypatch.setattr( - roster_controller.AgentRosterService, "active_config_is_published", lambda _self, **kwargs: False - ) monkeypatch.setattr( roster_controller.FeatureService, "get_system_features", @@ -1299,13 +1297,14 @@ def test_agent_composer_routes_resolve_app_from_agent_id( composer_controller.AgentComposerService, "collect_validation_findings", collect_validation_findings ) monkeypatch.setattr(composer_controller.AgentComposerService, "get_agent_app_candidates", get_agent_app_candidates) - assert unwrap(AgentComposerApi.get)(AgentComposerApi(), MagicMock(), "tenant-1", agent_id)["variant"] == "agent_app" + composer = unwrap(AgentComposerApi.get)(AgentComposerApi(), MagicMock(), "tenant-1", agent_id) + assert composer["variant"] == "agent_app" + assert composer["active_config_is_published"] is True assert cast(dict[str, object], captured["load"])["agent_id"] == agent_id with app.test_request_context(json=payload): - assert ( - unwrap(AgentComposerApi.put)(AgentComposerApi(), MagicMock(), "tenant-1", account_id, agent_id)["variant"] - == "agent_app" - ) + saved_composer = unwrap(AgentComposerApi.put)(AgentComposerApi(), MagicMock(), "tenant-1", account_id, agent_id) + assert saved_composer["variant"] == "agent_app" + assert saved_composer["active_config_is_published"] is True assert cast(dict[str, object], captured["save"])["agent_id"] == agent_id assert unwrap(AgentComposerValidateApi.post)(AgentComposerValidateApi(), MagicMock(), "tenant-1", agent_id) == { "result": "success", diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index c1946968e67..f881e8770dd 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -548,6 +548,7 @@ def test_load_agent_app_composer_exposes_draft_save_only(monkeypatch: pytest.Mon agent = SimpleNamespace( id="agent-1", active_config_snapshot_id="version-1", + active_config_is_published=True, updated_by="account-1", created_by="account-1", app_id="app-1", @@ -567,6 +568,7 @@ def test_load_agent_app_composer_exposes_draft_save_only(monkeypatch: pytest.Mon result = AgentComposerService.load_agent_app_composer(session=session, tenant_id="tenant-1", app_id="app-1") assert result["save_options"] == [ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value] + assert result["active_config_is_published"] is True def test_save_agent_app_composer_rejects_version_save_strategy(): @@ -610,7 +612,11 @@ def test_save_agent_app_composer_updates_normal_draft(monkeypatch: pytest.Monkey lambda **kwargs: saved.update(kwargs) or SimpleNamespace(id="draft-1"), ) monkeypatch.setattr(AgentComposerService, "_get_version_if_present", lambda **_kwargs: active_version) - monkeypatch.setattr(AgentComposerService, "load_agent_composer", lambda **kwargs: {"loaded": True}) + monkeypatch.setattr( + AgentComposerService, + "load_agent_composer", + lambda **kwargs: {"loaded": True, "active_config_is_published": agent.active_config_is_published}, + ) payload = ComposerSavePayload.model_validate( { "variant": ComposerVariant.AGENT_APP.value, @@ -628,7 +634,7 @@ def test_save_agent_app_composer_updates_normal_draft(monkeypatch: pytest.Monkey ) assert result.pop("validation") == {"warnings": [], "knowledge_retrieval_placeholder": []} - assert result == {"loaded": True} + assert result == {"loaded": True, "active_config_is_published": False} assert saved["draft_type"] == AgentConfigDraftType.DRAFT assert saved["agent_soul"].model_dump(mode="json") == _agent_soul_with_model().model_dump(mode="json") assert agent.active_config_is_published is False @@ -657,7 +663,11 @@ def test_save_agent_app_composer_keeps_published_when_draft_matches_active_snaps lambda **_kwargs: SimpleNamespace(id="draft-1"), ) monkeypatch.setattr(AgentComposerService, "_get_version_if_present", lambda **_kwargs: active_version) - monkeypatch.setattr(AgentComposerService, "load_agent_composer", lambda **_kwargs: {"loaded": True}) + monkeypatch.setattr( + AgentComposerService, + "load_agent_composer", + lambda **_kwargs: {"loaded": True, "active_config_is_published": agent.active_config_is_published}, + ) payload = ComposerSavePayload.model_validate( { "variant": ComposerVariant.AGENT_APP.value, @@ -666,7 +676,7 @@ def test_save_agent_app_composer_keeps_published_when_draft_matches_active_snaps } ) - AgentComposerService.save_agent_app_composer( + result = AgentComposerService.save_agent_app_composer( session=session, tenant_id="tenant-1", app_id="app-1", @@ -675,6 +685,7 @@ def test_save_agent_app_composer_keeps_published_when_draft_matches_active_snaps ) assert agent.active_config_is_published is True + assert result["active_config_is_published"] is True assert fake_session.flushes >= 1 diff --git a/e2e/features/step-definitions/agent-v2/agent-edit.steps.ts b/e2e/features/step-definitions/agent-v2/agent-edit.steps.ts index bce34d4ff54..06eb5291323 100644 --- a/e2e/features/step-definitions/agent-v2/agent-edit.steps.ts +++ b/e2e/features/step-definitions/agent-v2/agent-edit.steps.ts @@ -31,6 +31,7 @@ const getComposerInheritanceSnapshot = async (world: DifyWorld, agentId: string) const knowledgeSets = asArray(asRecord(soul.knowledge).sets) return { + activeConfigIsPublished: draft.active_config_is_published, fileNames: files .map((file) => asString(asRecord(file).name)) .filter(Boolean) @@ -189,8 +190,7 @@ Then( ) const client = this.getConsoleClient() - const [sourceDetail, duplicatedDetail, sourceSnapshot, duplicatedSnapshot] = await Promise.all([ - client.agent.byAgentId.get({ params: { agent_id: sourceAgent.id } }), + const [duplicatedDetail, sourceSnapshot, duplicatedSnapshot] = await Promise.all([ client.agent.byAgentId.get({ params: { agent_id: duplicatedAgentId } }), getComposerInheritanceSnapshot(this, sourceAgent.id), getComposerInheritanceSnapshot(this, duplicatedAgentId), @@ -198,9 +198,7 @@ Then( expect(duplicatedDetail.id).toBe(duplicatedAgentId) expect(duplicatedDetail.name).toBe(this.lastCreatedAgentName) - expect(duplicatedDetail.active_config_is_published).toBe( - sourceDetail.active_config_is_published, - ) + expect(duplicatedSnapshot.activeConfigIsPublished).toBe(sourceSnapshot.activeConfigIsPublished) expect(duplicatedSnapshot.model).toEqual({ name: stableModel.name, provider: stableModel.provider, diff --git a/e2e/features/step-definitions/agent-v2/publish.steps.ts b/e2e/features/step-definitions/agent-v2/publish.steps.ts index dcfaaf69e5f..db47cf0225b 100644 --- a/e2e/features/step-definitions/agent-v2/publish.steps.ts +++ b/e2e/features/step-definitions/agent-v2/publish.steps.ts @@ -32,10 +32,10 @@ Then('the Agent v2 draft should remain unpublished', async function (this: DifyW .poll( async () => { const agentId = getCurrentAgentId(this) - const agent = await this.getConsoleClient().agent.byAgentId.get({ + const composer = await this.getConsoleClient().agent.byAgentId.composer.get({ params: { agent_id: agentId }, }) - return agent.active_config_is_published + return composer.active_config_is_published }, { timeout: 30_000 }, ) @@ -55,10 +55,10 @@ Then('the Agent v2 draft should be published and up to date', async function (th await expect(page.getByText('Up to date')).toBeVisible() await expect .poll(async () => { - const agent = await this.getConsoleClient().agent.byAgentId.get({ + const composer = await this.getConsoleClient().agent.byAgentId.composer.get({ params: { agent_id: agentId }, }) - return agent.active_config_is_published + return composer.active_config_is_published }) .toBe(true) }) diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 98fda2ccab9..0a2bae65ac1 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -23,7 +23,6 @@ export type AgentAppCreatePayload = { export type AgentAppDetailWithSite = { access_mode?: string | null - active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null backing_app_id?: string | null @@ -169,6 +168,7 @@ export type SuggestedQuestionsResponse = { } export type AgentAppComposerResponse = { + active_config_is_published: boolean active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null agent: AgentComposerAgentResponse agent_soul: AgentSoulConfig @@ -1892,7 +1892,6 @@ export type AgentAppPaginationWritable = { export type AgentAppDetailWithSiteWritable = { access_mode?: string | null - active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null backing_app_id?: string | null diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 74d05d36775..dfa6917bfb7 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -374,7 +374,6 @@ export const zWorkflowPartial = z.object({ */ export const zAgentAppDetailWithSite = z.object({ access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), backing_app_id: z.string().nullish(), @@ -2494,6 +2493,7 @@ export const zComposerSavePayload = z.object({ * AgentAppComposerResponse */ export const zAgentAppComposerResponse = z.object({ + active_config_is_published: z.boolean(), active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(), agent: zAgentComposerAgentResponse, agent_soul: zAgentSoulConfig, @@ -2730,7 +2730,6 @@ export const zAppDetailSiteResponseWritable = z.object({ */ export const zAgentAppDetailWithSiteWritable = z.object({ access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), backing_app_id: z.string().nullish(), diff --git a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx index 0874ad7ac48..c0db1aa220c 100644 --- a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx @@ -447,7 +447,7 @@ function AgentConfigurePageComposerContent({ leftPanel={ { const createAgent = (overrides: Partial = {}): AgentMutationResponse => ({ ...overrides, - active_config_is_published: overrides.active_config_is_published ?? false, debug_conversation_has_messages: overrides.debug_conversation_has_messages ?? false, debug_conversation_message_count: overrides.debug_conversation_message_count ?? 0, enable_api: overrides.enable_api ?? true, @@ -125,6 +124,7 @@ const createAgent = (overrides: Partial = {}): AgentMutat const createComposerState = ( overrides: Partial = {}, ): AgentComposerMutationResponse => ({ + active_config_is_published: false, active_config_snapshot: { id: 'snapshot-1', version: 1,