mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 13:01:16 +08:00
fix(agent): save workflow agents as console roster apps (#37848)
This commit is contained in:
parent
d0b2239c60
commit
1eafbf9763
@ -197,7 +197,7 @@ class AgentAppRuntimeRequestBuilder:
|
||||
def _plugin_daemon_plugin_id(*, plugin_id: str, model_provider: str) -> str:
|
||||
"""Return the transport plugin id expected by plugin-daemon headers."""
|
||||
if plugin_id.count("/") == 1:
|
||||
return plugin_id
|
||||
return plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
if plugin_id:
|
||||
return ModelProviderID(plugin_id).plugin_id
|
||||
return ModelProviderID(model_provider).plugin_id
|
||||
|
||||
@ -265,7 +265,7 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
def _plugin_daemon_plugin_id(*, plugin_id: str, model_provider: str) -> str:
|
||||
"""Return the transport plugin id expected by plugin-daemon headers."""
|
||||
if plugin_id.count("/") == 1:
|
||||
return plugin_id
|
||||
return plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
if plugin_id:
|
||||
return ModelProviderID(plugin_id).plugin_id
|
||||
return ModelProviderID(model_provider).plugin_id
|
||||
|
||||
@ -8,6 +8,7 @@ from sqlalchemy.sql.elements import ColumnElement
|
||||
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import to_timestamp
|
||||
from models import Account
|
||||
from models.agent import (
|
||||
Agent,
|
||||
AgentConfigRevision,
|
||||
@ -36,6 +37,8 @@ from services.agent.errors import (
|
||||
AgentVersionNotFoundError,
|
||||
InvalidComposerConfigError,
|
||||
)
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.app_service import AppService, CreateAppParams
|
||||
from services.entities.agent_entities import (
|
||||
AgentSoulConfig,
|
||||
ComposerCandidatesResponse,
|
||||
@ -992,6 +995,14 @@ class AgentComposerService:
|
||||
operation=AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
version_note=payload.version_note,
|
||||
)
|
||||
cls._copy_agent_drive_rows(
|
||||
tenant_id=tenant_id,
|
||||
source_agent_id=source_agent.id,
|
||||
target_agent_id=roster_agent.id,
|
||||
account_id=account_id,
|
||||
agent_soul=agent_soul,
|
||||
node_job=payload.node_job or WorkflowNodeJobConfig.model_validate(binding.node_job_config_dict),
|
||||
)
|
||||
binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT
|
||||
binding.agent_id = roster_agent.id
|
||||
binding.current_snapshot_id = roster_agent.active_config_snapshot_id
|
||||
@ -1157,30 +1168,36 @@ class AgentComposerService:
|
||||
icon: str | None = None,
|
||||
icon_background: str | None = None,
|
||||
) -> Agent:
|
||||
agent = Agent(
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description=description,
|
||||
role=role,
|
||||
icon_type=icon_type,
|
||||
icon=icon,
|
||||
icon_background=icon_background,
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.WORKFLOW,
|
||||
status=AgentStatus.ACTIVE,
|
||||
created_by=account_id,
|
||||
updated_by=account_id,
|
||||
)
|
||||
db.session.add(agent)
|
||||
account = cls._require_account(account_id=account_id)
|
||||
try:
|
||||
db.session.flush()
|
||||
app = AppService().create_app(
|
||||
tenant_id,
|
||||
CreateAppParams(
|
||||
name=name,
|
||||
description=description,
|
||||
mode="agent",
|
||||
agent_role=role,
|
||||
icon_type=icon_type.value if isinstance(icon_type, AgentIconType) else icon_type,
|
||||
icon=icon,
|
||||
icon_background=icon_background,
|
||||
),
|
||||
account,
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
db.session.rollback()
|
||||
raise AgentNameConflictError() from exc
|
||||
version = cls._create_config_version(
|
||||
|
||||
agent = AgentRosterService(db.session).get_app_backing_agent(tenant_id=tenant_id, app_id=app.id)
|
||||
if agent is None:
|
||||
raise AgentNotFoundError()
|
||||
|
||||
current_snapshot = cls._require_version(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id,
|
||||
version_id=agent.active_config_snapshot_id,
|
||||
)
|
||||
version = cls._update_current_version(
|
||||
current_snapshot=current_snapshot,
|
||||
account_id=account_id,
|
||||
agent_soul=agent_soul,
|
||||
operation=operation,
|
||||
@ -1188,6 +1205,7 @@ class AgentComposerService:
|
||||
)
|
||||
agent.active_config_snapshot_id = version.id
|
||||
agent.active_config_has_model = agent_soul_has_model(agent_soul)
|
||||
agent.updated_by = account_id
|
||||
return agent
|
||||
|
||||
@classmethod
|
||||
@ -1316,6 +1334,13 @@ class AgentComposerService:
|
||||
raise AgentNotFoundError()
|
||||
return agent
|
||||
|
||||
@classmethod
|
||||
def _require_account(cls, *, account_id: str) -> Account:
|
||||
account = db.session.get(Account, account_id)
|
||||
if not account:
|
||||
raise ValueError("Account not found")
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def _get_agent_if_present(cls, *, tenant_id: str, agent_id: str | None) -> Agent | None:
|
||||
if not agent_id:
|
||||
|
||||
@ -837,6 +837,7 @@ class AgentRosterService:
|
||||
if agent.source == AgentSource.AGENT_APP:
|
||||
return {
|
||||
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
AgentConfigRevisionOperation.RESTORE_VERSION,
|
||||
}
|
||||
return {
|
||||
|
||||
@ -144,6 +144,22 @@ class TestAgentAppRuntimeRequestBuilder:
|
||||
assert result.redacted_request["composition"]["layers"][-1]["config"]["credentials"] == "[REDACTED]"
|
||||
assert result.metadata["conversation_id"] == "conv-1"
|
||||
|
||||
def test_build_normalizes_marketplace_model_plugin_id(self):
|
||||
soul = _soul_with_model()
|
||||
soul.model.plugin_id = (
|
||||
"langgenius/openai:0.4.2@21195ee1321849e0a7d4b3f6b2fd8c2be23ea6c7182e1b444ecc4c1711b52468"
|
||||
)
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
plugin_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = builder.build(_ctx(soul))
|
||||
|
||||
llm = next(layer for layer in result.request.composition.layers if layer.name == "llm")
|
||||
assert llm.config.plugin_id == "langgenius/openai"
|
||||
assert llm.config.model_provider == "openai"
|
||||
|
||||
def test_build_maps_agent_soul_knowledge_to_knowledge_layer(self):
|
||||
soul = AgentSoulConfig.model_validate(
|
||||
{
|
||||
|
||||
@ -189,7 +189,7 @@ def test_normalizes_langgenius_model_provider_for_agent_backend_transport():
|
||||
context.snapshot.config_snapshot = AgentSoulConfig(
|
||||
prompt={"system_prompt": "You are careful."},
|
||||
model=AgentSoulModelConfig(
|
||||
plugin_id="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai:0.4.2@21195ee1321849e0a7d4b3f6b2fd8c2be23ea6c7182e1b444ecc4c1711b52468",
|
||||
model_provider="langgenius/openai/openai",
|
||||
model="gpt-test",
|
||||
),
|
||||
|
||||
@ -3,6 +3,7 @@ from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from core.workflow.nodes.agent_v2.validators import WorkflowAgentNodeValidationError
|
||||
from models.agent import (
|
||||
@ -32,7 +33,12 @@ from services.agent import composer_service, roster_service
|
||||
from services.agent.agent_soul_state import agent_soul_has_model
|
||||
from services.agent.composer_service import AgentComposerService
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import AgentVersionConflictError, InvalidComposerConfigError
|
||||
from services.agent.errors import (
|
||||
AgentNameConflictError,
|
||||
AgentNotFoundError,
|
||||
AgentVersionConflictError,
|
||||
InvalidComposerConfigError,
|
||||
)
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.agent.workflow_publish_service import WorkflowAgentPublishService
|
||||
from services.app_service import AppListParams, AppService
|
||||
@ -427,6 +433,7 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.Monk
|
||||
icon_background="#FFFFFF",
|
||||
)
|
||||
create_roster_calls = []
|
||||
copy_drive_calls = []
|
||||
monkeypatch.setattr(AgentComposerService, "_create_workflow_only_agent", lambda **kwargs: workflow_agent)
|
||||
|
||||
def fake_create_roster_agent_for_composer(**kwargs):
|
||||
@ -438,6 +445,11 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.Monk
|
||||
"_create_roster_agent_for_composer",
|
||||
fake_create_roster_agent_for_composer,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_copy_agent_drive_rows",
|
||||
lambda **kwargs: copy_drive_calls.append(kwargs),
|
||||
)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_agent", lambda **kwargs: roster_agent)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
@ -533,6 +545,16 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.Monk
|
||||
assert create_roster_calls[1]["role"] == "Copied role"
|
||||
assert create_roster_calls[1]["icon"] == "copied"
|
||||
assert create_roster_calls[1]["icon_background"] == "#E0F2FE"
|
||||
assert copy_drive_calls == [
|
||||
{
|
||||
"tenant_id": "tenant-1",
|
||||
"source_agent_id": "roster-agent-1",
|
||||
"target_agent_id": "roster-agent-1",
|
||||
"account_id": "account-1",
|
||||
"agent_soul": payload.agent_soul,
|
||||
"node_job": payload.node_job,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_node_job_only_updates_inline_agent_soul(monkeypatch: pytest.MonkeyPatch):
|
||||
@ -1173,6 +1195,39 @@ def test_drive_copy_scopes_include_declared_output_benchmark_files():
|
||||
def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
created_apps = []
|
||||
backing_agent = Agent(
|
||||
id="roster-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Ready Agent",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
app_id="app-agent-1",
|
||||
active_config_snapshot_id="empty-version-1",
|
||||
)
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id, params, account):
|
||||
created_apps.append((tenant_id, params, account))
|
||||
return SimpleNamespace(id="app-agent-1")
|
||||
|
||||
class FakeAgentRosterService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def get_app_backing_agent(self, *, tenant_id, app_id):
|
||||
assert tenant_id == "tenant-1"
|
||||
assert app_id == "app-agent-1"
|
||||
return backing_agent
|
||||
|
||||
monkeypatch.setattr(composer_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(composer_service, "AgentRosterService", FakeAgentRosterService)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_account", lambda **kwargs: SimpleNamespace(id="account-1"))
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_require_version",
|
||||
lambda **kwargs: SimpleNamespace(id="empty-version-1", tenant_id="tenant-1", agent_id="roster-agent-1"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_create_config_version",
|
||||
@ -1200,6 +1255,81 @@ def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytes
|
||||
assert workflow_agent.active_config_has_model is True
|
||||
assert roster_agent.active_config_snapshot_id == "version-with-model"
|
||||
assert roster_agent.active_config_has_model is True
|
||||
assert roster_agent.source == AgentSource.AGENT_APP
|
||||
assert roster_agent.app_id == "app-agent-1"
|
||||
created_tenant_id, created_params, created_account = created_apps[0]
|
||||
assert created_tenant_id == "tenant-1"
|
||||
assert created_params.mode == "agent"
|
||||
assert created_params.name == "Ready Agent"
|
||||
assert created_account.id == "account-1"
|
||||
|
||||
|
||||
def test_composer_require_account(monkeypatch: pytest.MonkeyPatch):
|
||||
account = SimpleNamespace(id="account-1")
|
||||
monkeypatch.setattr(composer_service.db, "session", SimpleNamespace(get=lambda model, account_id: account))
|
||||
|
||||
assert AgentComposerService._require_account(account_id="account-1") is account
|
||||
|
||||
|
||||
def test_composer_require_account_raises_when_missing(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(composer_service.db, "session", SimpleNamespace(get=lambda model, account_id: None))
|
||||
|
||||
with pytest.raises(ValueError, match="Account not found"):
|
||||
AgentComposerService._require_account(account_id="missing-account")
|
||||
|
||||
|
||||
def test_composer_create_roster_agent_rolls_back_name_conflict(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id, params, account):
|
||||
raise IntegrityError("insert apps", params, Exception("duplicate"))
|
||||
|
||||
monkeypatch.setattr(composer_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_account", lambda **kwargs: SimpleNamespace(id="account-1"))
|
||||
|
||||
with pytest.raises(AgentNameConflictError):
|
||||
AgentComposerService._create_roster_agent_for_composer(
|
||||
tenant_id="tenant-1",
|
||||
account_id="account-1",
|
||||
name="Duplicate Agent",
|
||||
agent_soul=_agent_soul_with_model(),
|
||||
operation=AgentConfigRevisionOperation.CREATE_VERSION,
|
||||
version_note=None,
|
||||
)
|
||||
|
||||
assert fake_session.rollbacks == 1
|
||||
|
||||
|
||||
def test_composer_create_roster_agent_raises_when_backing_agent_missing(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id, params, account):
|
||||
return SimpleNamespace(id="app-agent-1")
|
||||
|
||||
class FakeAgentRosterService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def get_app_backing_agent(self, *, tenant_id, app_id):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(composer_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(composer_service, "AgentRosterService", FakeAgentRosterService)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_account", lambda **kwargs: SimpleNamespace(id="account-1"))
|
||||
|
||||
with pytest.raises(AgentNotFoundError):
|
||||
AgentComposerService._create_roster_agent_for_composer(
|
||||
tenant_id="tenant-1",
|
||||
account_id="account-1",
|
||||
name="Missing Backing Agent",
|
||||
agent_soul=_agent_soul_with_model(),
|
||||
operation=AgentConfigRevisionOperation.CREATE_VERSION,
|
||||
version_note=None,
|
||||
)
|
||||
|
||||
|
||||
def test_composer_version_helpers_and_lookup_errors(monkeypatch: pytest.MonkeyPatch):
|
||||
@ -1773,6 +1903,7 @@ def test_agent_app_visible_versions_exclude_draft_saves():
|
||||
|
||||
assert agent_app_operations == {
|
||||
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
AgentConfigRevisionOperation.RESTORE_VERSION,
|
||||
}
|
||||
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in agent_app_operations
|
||||
|
||||
Loading…
Reference in New Issue
Block a user