Files
dify/api/fields/agent_fields.py
Yansong Zhang ba8ddff696 feat(api): composer slash validations — human must be referenced + knowledge placeholder — ENG-617
Hard rule (PRD: human involvement must be slash-referenced or save errors):
every configured human contact must appear as {{#human:<id>#}} in its surface's
prompt (matched via any identity alias: id/contact_id/human_id/email/name;
identity-less contacts are skipped). Error code human_involvement_not_referenced,
enforced on both the agent soul prompt and the workflow job prompt.

Soft findings (never block save), returned by validate and attached to save
responses as `validation`:
- knowledge_retrieval_placeholder: dangling {{#knowledge:<id>#}} mentions (not
  in config, or configured-but-deleted in DB) are kept with a placeholder name
  (mention label, fallback "Knowledge <id8>") per the 0522 consensus, instead of
  being dropped or rejected; knowledge not referenced is fine.
- mention_target_missing: skill/file/tool/cli_tool/human/node_output/output
  mentions whose id resolves to no configured item.
- mention_malformed: mention-shaped markers the strict grammar rejects
  (degraded to plain text at runtime by the scrub pass).

Response models gain warnings/knowledge_retrieval_placeholder (validate) and
validation (save); validate endpoints now resolve dataset existence via the
tenant-scoped dataset lookup.

Design: Confluence 490012681 §5. Depends on the ENG-616 mention contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:51:31 +08:00

230 lines
7.0 KiB
Python

from typing import Literal
from pydantic import Field
from fields.base import ResponseModel
from models.agent import (
AgentConfigRevisionOperation,
AgentIconType,
AgentKind,
AgentScope,
AgentSource,
AgentStatus,
WorkflowAgentBindingType,
)
from models.agent_config_entities import (
AgentCliToolConfig,
AgentHumanContactConfig,
AgentKnowledgeDatasetConfig,
AgentSkillRefConfig,
AgentSoulConfig,
DeclaredOutputConfig,
DeclaredOutputType,
WorkflowNodeJobConfig,
WorkflowPreviousNodeOutputRef,
)
from services.entities.agent_entities import (
ComposerCandidateCapabilities,
ComposerSaveStrategy,
ComposerVariant,
)
class AgentConfigSnapshotSummaryResponse(ResponseModel):
id: str
agent_id: str | None = None
version: int
summary: str | None = None
version_note: str | None = None
created_by: str | None = None
created_at: int | None = None
class AgentRosterResponse(ResponseModel):
id: str
name: str
description: str
icon_type: AgentIconType | None = None
icon: str | None = None
icon_background: str | None = None
agent_kind: AgentKind
scope: AgentScope
source: AgentSource
app_id: str | None = None
workflow_id: str | None = None
workflow_node_id: str | None = None
active_config_snapshot_id: str | None = None
active_config_snapshot: AgentConfigSnapshotSummaryResponse | None = None
status: AgentStatus
created_by: str | None = None
updated_by: str | None = None
archived_by: str | None = None
archived_at: int | None = None
created_at: int | None = None
updated_at: int | None = None
class AgentInviteOptionResponse(AgentRosterResponse):
is_in_current_workflow: bool = False
in_current_workflow_count: int = 0
existing_node_ids: list[str] = Field(default_factory=list)
class AgentRosterListResponse(ResponseModel):
data: list[AgentRosterResponse]
page: int
limit: int
total: int
has_more: bool
class AgentInviteOptionsResponse(ResponseModel):
data: list[AgentInviteOptionResponse]
page: int
limit: int
total: int
has_more: bool
class AgentConfigRevisionResponse(ResponseModel):
id: str
previous_snapshot_id: str | None = None
current_snapshot_id: str
revision: int
operation: AgentConfigRevisionOperation
summary: str | None = None
version_note: str | None = None
created_by: str | None = None
created_at: int | None = None
class AgentConfigSnapshotDetailResponse(AgentConfigSnapshotSummaryResponse):
config_snapshot: AgentSoulConfig
revisions: list[AgentConfigRevisionResponse] = Field(default_factory=list)
class AgentConfigSnapshotListResponse(ResponseModel):
data: list[AgentConfigSnapshotSummaryResponse]
class AgentComposerAgentResponse(ResponseModel):
id: str
name: str
description: str
scope: AgentScope
status: AgentStatus
active_config_snapshot_id: str | None = None
class AgentComposerBindingResponse(ResponseModel):
id: str
binding_type: WorkflowAgentBindingType
agent_id: str | None = None
current_snapshot_id: str | None = None
workflow_id: str
node_id: str
class AgentComposerSoulLockResponse(ResponseModel):
locked: bool
can_unlock: bool = False
reason: str | None = None
class AgentComposerImpactBindingResponse(ResponseModel):
app_id: str
workflow_id: str
node_id: str
class AgentComposerImpactResponse(ResponseModel):
current_snapshot_id: str | None = None
workflow_node_count: int
bindings: list[AgentComposerImpactBindingResponse] = Field(default_factory=list)
class WorkflowAgentComposerResponse(ResponseModel):
variant: Literal[ComposerVariant.WORKFLOW]
agent: AgentComposerAgentResponse | None = None
active_config_snapshot: AgentConfigSnapshotSummaryResponse | None = None
binding: AgentComposerBindingResponse | None = None
soul_lock: AgentComposerSoulLockResponse
agent_soul: AgentSoulConfig
node_job: WorkflowNodeJobConfig
effective_declared_outputs: list[DeclaredOutputConfig] = Field(default_factory=list)
save_options: list[ComposerSaveStrategy]
impact_summary: AgentComposerImpactResponse | None = None
validation: "ComposerValidationFindingsResponse | None" = None
app_id: str | None = None
workflow_id: str | None = None
node_id: str | None = None
class AgentAppComposerResponse(ResponseModel):
variant: Literal[ComposerVariant.AGENT_APP]
agent: AgentComposerAgentResponse
active_config_snapshot: AgentConfigSnapshotSummaryResponse
agent_soul: AgentSoulConfig
save_options: list[ComposerSaveStrategy]
validation: "ComposerValidationFindingsResponse | None" = None
class ComposerValidationWarningResponse(ResponseModel):
code: str
surface: str | None = None
kind: str | None = None
id: str | None = None
message: str | None = None
class ComposerKnowledgePlaceholderResponse(ResponseModel):
id: str
placeholder_name: str
class ComposerValidationFindingsResponse(ResponseModel):
warnings: list[ComposerValidationWarningResponse] = Field(default_factory=list)
knowledge_retrieval_placeholder: list[ComposerKnowledgePlaceholderResponse] = Field(default_factory=list)
class AgentComposerValidateResponse(ResponseModel):
result: Literal["success"]
errors: list[str] = Field(default_factory=list)
warnings: list[ComposerValidationWarningResponse] = Field(default_factory=list)
knowledge_retrieval_placeholder: list[ComposerKnowledgePlaceholderResponse] = Field(default_factory=list)
class AgentComposerDifyToolCandidateResponse(ResponseModel):
id: str | None = None
name: str | None = None
description: str | None = None
provider: str | None = None
provider_id: str | None = None
plugin_id: str | None = None
class AgentComposerNodeJobCandidatesResponse(ResponseModel):
previous_node_outputs: list[WorkflowPreviousNodeOutputRef] = Field(default_factory=list)
declare_output_types: list[DeclaredOutputType] = Field(default_factory=list)
human_contacts: list[AgentHumanContactConfig] = Field(default_factory=list)
class AgentComposerSoulCandidatesResponse(ResponseModel):
skills_files: list[AgentSkillRefConfig] = Field(default_factory=list)
dify_tools: list[AgentComposerDifyToolCandidateResponse] = Field(default_factory=list)
cli_tools: list[AgentCliToolConfig] = Field(default_factory=list)
knowledge_datasets: list[AgentKnowledgeDatasetConfig] = Field(default_factory=list)
human_contacts: list[AgentHumanContactConfig] = Field(default_factory=list)
class AgentComposerCandidatesResponse(ResponseModel):
variant: ComposerVariant
allowed_node_job_candidates: AgentComposerNodeJobCandidatesResponse = Field(
default_factory=AgentComposerNodeJobCandidatesResponse
)
allowed_soul_candidates: AgentComposerSoulCandidatesResponse = Field(
default_factory=AgentComposerSoulCandidatesResponse
)
capabilities: ComposerCandidateCapabilities = Field(default_factory=ComposerCandidateCapabilities)
truncated: bool = False