mirror of
https://github.com/langgenius/dify.git
synced 2026-07-26 22:28:32 +08:00
fix(agent): refresh stale inline preview drafts (#39415)
This commit is contained in:
parent
8853ed99a5
commit
4da4fa72cd
@ -682,42 +682,27 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
if draft_type == AgentConfigDraftType.DEBUG_BUILD.value
|
||||
else AgentConfigDraftType.DRAFT
|
||||
)
|
||||
if effective_draft_type == AgentConfigDraftType.DRAFT:
|
||||
from services.agent.composer_service import AgentComposerService
|
||||
|
||||
return AgentComposerService.get_or_create_normal_agent_draft(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent=agent,
|
||||
created_by=agent.updated_by or agent.created_by,
|
||||
)
|
||||
if not account_id:
|
||||
raise AgentAppGeneratorError("Build draft requires an account user")
|
||||
stmt = select(AgentConfigDraft).where(
|
||||
AgentConfigDraft.tenant_id == tenant_id,
|
||||
AgentConfigDraft.agent_id == agent.id,
|
||||
AgentConfigDraft.draft_type == effective_draft_type,
|
||||
AgentConfigDraft.draft_type == AgentConfigDraftType.DEBUG_BUILD,
|
||||
AgentConfigDraft.account_id == account_id,
|
||||
)
|
||||
if effective_draft_type == AgentConfigDraftType.DEBUG_BUILD:
|
||||
if not account_id:
|
||||
raise AgentAppGeneratorError("Build draft requires an account user")
|
||||
stmt = stmt.where(AgentConfigDraft.account_id == account_id)
|
||||
else:
|
||||
stmt = stmt.where(AgentConfigDraft.account_id.is_(None))
|
||||
draft = session.scalar(stmt.order_by(AgentConfigDraft.updated_at.desc()).limit(1))
|
||||
if draft is not None:
|
||||
return draft
|
||||
if effective_draft_type == AgentConfigDraftType.DEBUG_BUILD:
|
||||
raise AgentAppGeneratorError("Agent build draft not found")
|
||||
_, snapshot, agent_soul = AgentAppGenerator._resolve_agent_by_id(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id,
|
||||
snapshot_id=agent.active_config_snapshot_id,
|
||||
session=session,
|
||||
)
|
||||
draft = AgentConfigDraft(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id,
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
draft_owner_key="",
|
||||
base_snapshot_id=snapshot.id,
|
||||
config_snapshot=agent_soul,
|
||||
created_by=agent.created_by,
|
||||
updated_by=agent.updated_by,
|
||||
)
|
||||
session.add(draft)
|
||||
session.flush()
|
||||
return draft
|
||||
raise AgentAppGeneratorError("Agent build draft not found")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_agent_by_id(
|
||||
|
||||
@ -1164,6 +1164,20 @@ class AgentComposerService:
|
||||
agent.active_config_is_published = True
|
||||
agent.updated_by = account_id
|
||||
binding.current_snapshot_id = version.id
|
||||
normal_draft = cls._get_agent_draft(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id,
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
)
|
||||
if normal_draft is not None and cls._rebase_workflow_only_normal_draft(
|
||||
agent=agent,
|
||||
draft=normal_draft,
|
||||
snapshot=version,
|
||||
updated_by=account_id,
|
||||
):
|
||||
session.flush()
|
||||
binding.updated_by = account_id
|
||||
return binding
|
||||
|
||||
@ -1748,6 +1762,47 @@ class AgentComposerService:
|
||||
stmt = stmt.where(AgentConfigDraft.account_id.is_(None))
|
||||
return session.scalar(stmt.order_by(AgentConfigDraft.updated_at.desc()).limit(1))
|
||||
|
||||
@classmethod
|
||||
def get_or_create_normal_agent_draft(
|
||||
cls,
|
||||
*,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
agent: Agent,
|
||||
created_by: str | None,
|
||||
) -> AgentConfigDraft:
|
||||
"""Resolve the shared Preview draft, rebasing inline agents when needed."""
|
||||
return cls._get_or_create_agent_draft(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent=agent,
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _rebase_workflow_only_normal_draft(
|
||||
*,
|
||||
agent: Agent,
|
||||
draft: AgentConfigDraft,
|
||||
snapshot: AgentConfigSnapshot,
|
||||
updated_by: str | None,
|
||||
) -> bool:
|
||||
if (
|
||||
agent.scope != AgentScope.WORKFLOW_ONLY
|
||||
or draft.draft_type != AgentConfigDraftType.DRAFT
|
||||
or draft.account_id is not None
|
||||
or not agent.active_config_snapshot_id
|
||||
or draft.base_snapshot_id == agent.active_config_snapshot_id
|
||||
or snapshot.id != agent.active_config_snapshot_id
|
||||
):
|
||||
return False
|
||||
draft.base_snapshot_id = snapshot.id
|
||||
draft.config_snapshot = AgentSoulConfig.model_validate(snapshot.config_snapshot_dict)
|
||||
draft.updated_by = updated_by
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _get_or_create_agent_draft(
|
||||
cls,
|
||||
@ -1767,6 +1822,26 @@ class AgentComposerService:
|
||||
account_id=account_id,
|
||||
)
|
||||
if draft is not None:
|
||||
if (
|
||||
agent.scope == AgentScope.WORKFLOW_ONLY
|
||||
and draft_type == AgentConfigDraftType.DRAFT
|
||||
and draft.account_id is None
|
||||
and agent.active_config_snapshot_id
|
||||
and draft.base_snapshot_id != agent.active_config_snapshot_id
|
||||
):
|
||||
active_snapshot = cls._get_version_if_present(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id,
|
||||
version_id=agent.active_config_snapshot_id,
|
||||
)
|
||||
if active_snapshot is not None and cls._rebase_workflow_only_normal_draft(
|
||||
agent=agent,
|
||||
draft=draft,
|
||||
snapshot=active_snapshot,
|
||||
updated_by=agent.updated_by or agent.created_by,
|
||||
):
|
||||
session.flush()
|
||||
return draft
|
||||
base_snapshot = cls._get_version_if_present(
|
||||
session=session,
|
||||
|
||||
@ -14,7 +14,8 @@ import pytest
|
||||
|
||||
from core.app.apps.agent_app.app_generator import AgentAppGenerator, AgentAppGeneratorError, AgentAppNotPublishedError
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models.agent import AgentConfigDraftType, AgentSource
|
||||
from models.agent import AgentConfigDraft, AgentConfigDraftType, AgentScope, AgentSource
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
|
||||
_SOUL_DICT = {
|
||||
"model": {
|
||||
@ -95,7 +96,7 @@ class TestResolveDebugDraft:
|
||||
created_by="creator-1",
|
||||
updated_by="updater-1",
|
||||
)
|
||||
session = _FakeScalarSession([None, SimpleNamespace(id="agent-1"), _snapshot()])
|
||||
session = _FakeScalarSession([None, _snapshot()])
|
||||
|
||||
draft = AgentAppGenerator._resolve_debug_draft(
|
||||
tenant_id="t1",
|
||||
@ -110,6 +111,77 @@ class TestResolveDebugDraft:
|
||||
assert session.added == [draft]
|
||||
assert session.flush_count == 1
|
||||
|
||||
def test_stale_workflow_only_shared_draft_is_rebased_to_active_snapshot(self):
|
||||
agent = SimpleNamespace(
|
||||
id="agent-1",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
active_config_snapshot_id="snap-2",
|
||||
created_by="creator-1",
|
||||
updated_by="updater-1",
|
||||
)
|
||||
draft = AgentConfigDraft(
|
||||
id="draft-1",
|
||||
tenant_id="t1",
|
||||
agent_id="agent-1",
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
draft_owner_key="",
|
||||
base_snapshot_id="snap-1",
|
||||
config_snapshot=AgentSoulConfig.model_validate({"prompt": {"system_prompt": "old"}}),
|
||||
)
|
||||
active_snapshot = SimpleNamespace(
|
||||
id="snap-2",
|
||||
config_snapshot_dict={"prompt": {"system_prompt": "new"}},
|
||||
)
|
||||
session = _FakeScalarSession([draft, active_snapshot])
|
||||
|
||||
resolved = AgentAppGenerator._resolve_debug_draft(
|
||||
tenant_id="t1",
|
||||
agent=agent,
|
||||
draft_type=None,
|
||||
account_id=None,
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert resolved is draft
|
||||
assert resolved.id == "draft-1"
|
||||
assert resolved.base_snapshot_id == "snap-2"
|
||||
assert resolved.config_snapshot_dict["prompt"]["system_prompt"] == "new"
|
||||
assert session.flush_count == 1
|
||||
|
||||
def test_build_draft_is_not_rebased_to_active_snapshot(self):
|
||||
agent = SimpleNamespace(
|
||||
id="agent-1",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
active_config_snapshot_id="snap-2",
|
||||
created_by="creator-1",
|
||||
updated_by="updater-1",
|
||||
)
|
||||
draft = AgentConfigDraft(
|
||||
id="build-draft-1",
|
||||
tenant_id="t1",
|
||||
agent_id="agent-1",
|
||||
draft_type=AgentConfigDraftType.DEBUG_BUILD,
|
||||
account_id="account-1",
|
||||
draft_owner_key="account-1",
|
||||
base_snapshot_id="snap-1",
|
||||
config_snapshot=AgentSoulConfig.model_validate({"prompt": {"system_prompt": "build edit"}}),
|
||||
)
|
||||
session = _FakeScalarSession([draft])
|
||||
|
||||
resolved = AgentAppGenerator._resolve_debug_draft(
|
||||
tenant_id="t1",
|
||||
agent=agent,
|
||||
draft_type=AgentConfigDraftType.DEBUG_BUILD.value,
|
||||
account_id="account-1",
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert resolved is draft
|
||||
assert resolved.base_snapshot_id == "snap-1"
|
||||
assert resolved.config_snapshot_dict["prompt"]["system_prompt"] == "build edit"
|
||||
assert session.flush_count == 0
|
||||
|
||||
|
||||
class TestResolveAgent:
|
||||
def test_success_chains_to_resolve_by_id(self):
|
||||
@ -185,9 +257,12 @@ class TestResolveAgent:
|
||||
def test_unpublished_imported_agent_remains_available_to_debugger(self):
|
||||
bound_agent = SimpleNamespace(
|
||||
id="agent-1",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.IMPORTED,
|
||||
active_config_snapshot_id="snap-1",
|
||||
active_config_is_published=False,
|
||||
created_by="creator-1",
|
||||
updated_by="updater-1",
|
||||
)
|
||||
draft = SimpleNamespace(id="draft-1", draft_type="draft", config_snapshot_dict=_SOUL_DICT)
|
||||
session = _FakeScalarSession([bound_agent, draft])
|
||||
|
||||
@ -1271,11 +1271,32 @@ def test_node_job_only_updates_inline_agent_soul(monkeypatch: pytest.MonkeyPatch
|
||||
tenant_id="tenant-1",
|
||||
agent_id="inline-agent-1",
|
||||
version=2,
|
||||
config_snapshot=AgentSoulConfig.model_validate(
|
||||
{
|
||||
"model": {
|
||||
"plugin_id": "langgenius/openai/openai",
|
||||
"model_provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
},
|
||||
"prompt": {"system_prompt": "new"},
|
||||
}
|
||||
),
|
||||
)
|
||||
normal_draft = AgentConfigDraft(
|
||||
id="draft-1",
|
||||
tenant_id="tenant-1",
|
||||
agent_id="inline-agent-1",
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
draft_owner_key="",
|
||||
base_snapshot_id="inline-version-1",
|
||||
config_snapshot=AgentSoulConfig.model_validate({"prompt": {"system_prompt": "old"}}),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(AgentComposerService, "_require_version", lambda **kwargs: current_snapshot)
|
||||
monkeypatch.setattr(AgentComposerService, "_update_current_version", lambda **kwargs: next_snapshot)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_agent", lambda **kwargs: inline_agent)
|
||||
monkeypatch.setattr(AgentComposerService, "_get_agent_draft", lambda **kwargs: normal_draft)
|
||||
|
||||
binding = WorkflowAgentNodeBinding(
|
||||
tenant_id="tenant-1",
|
||||
@ -1320,6 +1341,95 @@ def test_node_job_only_updates_inline_agent_soul(monkeypatch: pytest.MonkeyPatch
|
||||
assert inline_agent.active_config_snapshot_id == "inline-version-2"
|
||||
assert inline_agent.active_config_has_model is True
|
||||
assert inline_agent.updated_by == "account-1"
|
||||
assert normal_draft.id == "draft-1"
|
||||
assert normal_draft.base_snapshot_id == "inline-version-2"
|
||||
assert normal_draft.config_snapshot_dict == next_snapshot.config_snapshot_dict
|
||||
assert normal_draft.updated_by == "account-1"
|
||||
|
||||
|
||||
def test_get_or_create_normal_agent_draft_rebases_stale_workflow_only_draft():
|
||||
agent = Agent(
|
||||
id="inline-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Inline",
|
||||
description="",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
status=AgentStatus.ACTIVE,
|
||||
active_config_snapshot_id="inline-version-2",
|
||||
created_by="account-1",
|
||||
updated_by="account-2",
|
||||
)
|
||||
draft = AgentConfigDraft(
|
||||
id="draft-1",
|
||||
tenant_id="tenant-1",
|
||||
agent_id=agent.id,
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
draft_owner_key="",
|
||||
base_snapshot_id="inline-version-1",
|
||||
config_snapshot=AgentSoulConfig.model_validate({"prompt": {"system_prompt": "old"}}),
|
||||
)
|
||||
active_snapshot = AgentConfigSnapshot(
|
||||
id="inline-version-2",
|
||||
tenant_id="tenant-1",
|
||||
agent_id=agent.id,
|
||||
version=2,
|
||||
config_snapshot=AgentSoulConfig.model_validate({"prompt": {"system_prompt": "new"}}),
|
||||
)
|
||||
session = FakeSession(scalar=[draft, active_snapshot])
|
||||
|
||||
resolved = AgentComposerService.get_or_create_normal_agent_draft(
|
||||
session=session,
|
||||
tenant_id="tenant-1",
|
||||
agent=agent,
|
||||
created_by="account-2",
|
||||
)
|
||||
|
||||
assert resolved is draft
|
||||
assert resolved.id == "draft-1"
|
||||
assert resolved.base_snapshot_id == "inline-version-2"
|
||||
assert resolved.config_snapshot_dict == active_snapshot.config_snapshot_dict
|
||||
assert resolved.updated_by == "account-2"
|
||||
assert session.flushes == 1
|
||||
|
||||
|
||||
def test_get_or_create_normal_agent_draft_keeps_roster_draft_edits():
|
||||
agent = Agent(
|
||||
id="roster-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Roster",
|
||||
description="",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
active_config_snapshot_id="version-2",
|
||||
)
|
||||
draft = AgentConfigDraft(
|
||||
id="draft-1",
|
||||
tenant_id="tenant-1",
|
||||
agent_id=agent.id,
|
||||
draft_type=AgentConfigDraftType.DRAFT,
|
||||
account_id=None,
|
||||
draft_owner_key="",
|
||||
base_snapshot_id="version-1",
|
||||
config_snapshot=AgentSoulConfig.model_validate({"prompt": {"system_prompt": "local edit"}}),
|
||||
)
|
||||
session = FakeSession(scalar=[draft])
|
||||
|
||||
resolved = AgentComposerService.get_or_create_normal_agent_draft(
|
||||
session=session,
|
||||
tenant_id="tenant-1",
|
||||
agent=agent,
|
||||
created_by="account-1",
|
||||
)
|
||||
|
||||
assert resolved is draft
|
||||
assert resolved.base_snapshot_id == "version-1"
|
||||
assert resolved.config_snapshot_dict["prompt"]["system_prompt"] == "local edit"
|
||||
assert session.flushes == 0
|
||||
|
||||
|
||||
def test_node_job_only_switches_roster_binding_to_inline_agent(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user