fix(agent): refresh stale inline preview drafts (#39415)

This commit is contained in:
zyssyz123 2026-07-22 20:54:09 +08:00 committed by GitHub
parent 8853ed99a5
commit 4da4fa72cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 276 additions and 31 deletions

View File

@ -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(

View File

@ -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,

View File

@ -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])

View File

@ -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):