feat: add agent roster composer APIs

This commit is contained in:
Yansong Zhang
2026-05-19 11:18:58 +08:00
parent f9ae632d29
commit 689e835367
15 changed files with 2034 additions and 0 deletions

View File

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

View File

@ -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"] == []