mirror of
https://github.com/langgenius/dify.git
synced 2026-06-08 09:27:39 +08:00
feat: add agent roster composer APIs
This commit is contained in:
@ -7,6 +7,8 @@ from sqlalchemy.exc import IntegrityError
|
||||
from models.agent import (
|
||||
Agent,
|
||||
AgentConfigVersion,
|
||||
AgentConfigVersionOperation,
|
||||
AgentConfigVersionRevision,
|
||||
AgentKind,
|
||||
AgentScope,
|
||||
AgentSource,
|
||||
@ -25,6 +27,7 @@ def test_agent_enums_match_prd_boundaries():
|
||||
assert AgentSource.WORKFLOW.value == "workflow"
|
||||
assert AgentStatus.ACTIVE.value == "active"
|
||||
assert AgentStatus.ARCHIVED.value == "archived"
|
||||
assert AgentConfigVersionOperation.SAVE_CURRENT_VERSION.value == "save_current_version"
|
||||
assert WorkflowAgentBindingType.ROSTER_AGENT.value == "roster_agent"
|
||||
assert WorkflowAgentBindingType.INLINE_AGENT.value == "inline_agent"
|
||||
|
||||
@ -149,7 +152,36 @@ def test_long_text_columns_do_not_use_mysql_incompatible_server_defaults():
|
||||
for column in (
|
||||
Agent.__table__.c.description,
|
||||
AgentConfigVersion.__table__.c.config_snapshot,
|
||||
AgentConfigVersionRevision.__table__.c.config_snapshot,
|
||||
AgentConfigVersionRevision.__table__.c.previous_config_snapshot,
|
||||
WorkflowAgentNodeBinding.__table__.c.node_job_config,
|
||||
):
|
||||
assert isinstance(column.type, LongText)
|
||||
assert column.server_default is None
|
||||
|
||||
|
||||
def test_agent_config_version_revision_records_audit_snapshot():
|
||||
snapshot = {"schema_version": 1, "prompt": {"system_prompt": "new"}}
|
||||
previous_snapshot = {"schema_version": 1, "prompt": {"system_prompt": "old"}}
|
||||
revision = AgentConfigVersionRevision(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="agent-1",
|
||||
agent_config_version_id="version-1",
|
||||
revision=2,
|
||||
operation=AgentConfigVersionOperation.SAVE_CURRENT_VERSION,
|
||||
config_snapshot=json.dumps(snapshot),
|
||||
previous_config_snapshot=json.dumps(previous_snapshot),
|
||||
)
|
||||
|
||||
unique_constraints = {
|
||||
constraint.name: tuple(column.name for column in constraint.columns)
|
||||
for constraint in AgentConfigVersionRevision.__table__.constraints
|
||||
if constraint.__class__.__name__ == "UniqueConstraint"
|
||||
}
|
||||
|
||||
assert unique_constraints["agent_config_version_revision_version_revision_unique"] == (
|
||||
"agent_config_version_id",
|
||||
"revision",
|
||||
)
|
||||
assert revision.config_snapshot_dict == snapshot
|
||||
assert revision.previous_config_snapshot_dict == previous_snapshot
|
||||
|
||||
@ -0,0 +1,144 @@
|
||||
import pytest
|
||||
|
||||
from services.agent.composer_service import AgentComposerService
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import AgentSoulLockedError, PlaintextSecretNotAllowedError
|
||||
from services.entities.agent_entities import (
|
||||
AgentKnowledgeQueryMode,
|
||||
AgentSoulConfig,
|
||||
ComposerSavePayload,
|
||||
ComposerSaveStrategy,
|
||||
ComposerVariant,
|
||||
DeclaredOutputType,
|
||||
WorkflowNodeJobConfig,
|
||||
)
|
||||
|
||||
|
||||
def test_workflow_variant_rejects_agent_app_only_fields():
|
||||
with pytest.raises(ValueError):
|
||||
ComposerSavePayload.model_validate(
|
||||
{
|
||||
"variant": ComposerVariant.WORKFLOW,
|
||||
"save_strategy": ComposerSaveStrategy.NODE_JOB_ONLY,
|
||||
"agent_soul": {
|
||||
"app_variables": [{"name": "company_name", "type": "string"}],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_agent_app_variant_rejects_workflow_node_job():
|
||||
with pytest.raises(ValueError):
|
||||
ComposerSavePayload.model_validate(
|
||||
{
|
||||
"variant": ComposerVariant.AGENT_APP,
|
||||
"save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION,
|
||||
"node_job": {"workflow_prompt": "Use the previous node output."},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_locked_workflow_soul_rejects_soul_changes():
|
||||
payload = ComposerSavePayload.model_validate(
|
||||
{
|
||||
"variant": ComposerVariant.WORKFLOW,
|
||||
"save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION,
|
||||
"soul_lock": {"locked": True},
|
||||
"agent_soul": {"prompt": {"system_prompt": "changed"}},
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(AgentSoulLockedError):
|
||||
ComposerConfigValidator.validate_save_payload(payload)
|
||||
|
||||
|
||||
def test_agent_app_soul_allows_app_features_and_variables():
|
||||
payload = ComposerSavePayload.model_validate(
|
||||
{
|
||||
"variant": ComposerVariant.AGENT_APP,
|
||||
"save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION,
|
||||
"agent_soul": {
|
||||
"app_features": {
|
||||
"conversation_opener": {},
|
||||
"follow_up": {},
|
||||
"citations_and_attributions": {},
|
||||
"content_moderation": {},
|
||||
"annotation_reply": {},
|
||||
},
|
||||
"app_variables": [{"name": "company_name", "type": "string", "required": True}],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
ComposerConfigValidator.validate_save_payload(payload)
|
||||
assert payload.agent_soul is not None
|
||||
assert payload.agent_soul.app_variables[0].name == "company_name"
|
||||
|
||||
|
||||
def test_knowledge_query_mode_uses_stable_backend_enums():
|
||||
config = AgentSoulConfig.model_validate(
|
||||
{
|
||||
"knowledge": {
|
||||
"datasets": [{"dataset_id": "dataset-1"}],
|
||||
"query_mode": "generated_query",
|
||||
"query_config": {"generation_prompt": "Create a retrieval query."},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert config.knowledge.query_mode == AgentKnowledgeQueryMode.GENERATED_QUERY
|
||||
|
||||
|
||||
def test_declared_outputs_support_file_check_and_failure_strategy():
|
||||
node_job = WorkflowNodeJobConfig.model_validate(
|
||||
{
|
||||
"declared_outputs": [
|
||||
{
|
||||
"name": "analysis_report",
|
||||
"type": "file",
|
||||
"file": {"extensions": [".pdf"], "mime_types": ["application/pdf"]},
|
||||
"checks": [
|
||||
{
|
||||
"type": "benchmark_file",
|
||||
"prompt": "Report must include risk summary.",
|
||||
"benchmark_file_ref": {"upload_file_id": "file-1"},
|
||||
}
|
||||
],
|
||||
"failure_strategy": {
|
||||
"on_type_check_failed": "fail_node",
|
||||
"on_output_check_failed": "retry",
|
||||
"max_retries": 1,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
output = node_job.declared_outputs[0]
|
||||
assert output.type == DeclaredOutputType.FILE
|
||||
assert output.file is not None
|
||||
assert output.file.extensions == [".pdf"]
|
||||
assert output.checks[0].type == "benchmark_file"
|
||||
assert output.failure_strategy is not None
|
||||
assert output.failure_strategy.max_retries == 1
|
||||
|
||||
|
||||
def test_plaintext_secrets_are_rejected():
|
||||
config = AgentSoulConfig.model_validate({"env": {"variables": [{"name": "OPENAI_API_KEY", "api_key": "secret"}]}})
|
||||
|
||||
with pytest.raises(PlaintextSecretNotAllowedError):
|
||||
ComposerConfigValidator.validate_agent_soul(config)
|
||||
|
||||
|
||||
def test_workflow_agent_soul_config_strips_agent_app_only_fields():
|
||||
config = AgentComposerService._workflow_agent_soul_config(
|
||||
{
|
||||
"prompt": {"system_prompt": "answer carefully"},
|
||||
"app_features": {"conversation_opener": {"enabled": True}},
|
||||
"app_variables": [{"name": "company_name", "type": "string"}],
|
||||
}
|
||||
)
|
||||
|
||||
assert config["prompt"]["system_prompt"] == "answer carefully"
|
||||
assert config["app_features"] == {}
|
||||
assert config["app_variables"] == []
|
||||
Reference in New Issue
Block a user