dify/api/services/agent/dsl_entities.py
wangxiaolei 0024a845f7
feat: support agent skill (#39675)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: 起岚 <155608604+xiaoyaoqilan@users.noreply.github.com>
Co-authored-by: Parman Mohammadalizadeh <prmma23@gmail.com>
Co-authored-by: Escape0707 <tothesong@gmail.com>
Co-authored-by: Xin Zhang <zhangxin@dify.ai>
Co-authored-by: iridescentWen <66297467+iridescentWen@users.noreply.github.com>
Co-authored-by: Bond Zhu <37842169+MRZHUH@users.noreply.github.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Chester <superdiao6@gmail.com>
Co-authored-by: WH-2099 <wh2099@pm.me>
Co-authored-by: Jony <619963502@qq.com>
Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com>
Co-authored-by: Harsh Kashyap <harsh.kashyap2001@gmail.com>
Co-authored-by: Harsh Kashyap <Harsh23Kashyap@users.noreply.github.com>
Co-authored-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: csurong <csurong1@gmail.com>
Co-authored-by: caosurong <surong.cao@thinkingdata.cn>
Co-authored-by: Taranum Wasu <81034301+Taranum01@users.noreply.github.com>
Co-authored-by: Taranum01 <50813317+Taranum01@users.noreply.github.com>
Co-authored-by: Pranav Agarwal <agarwalpranav0711@gmail.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: zl86790 <marshal_li_b@163.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
2026-08-24 05:29:14 +00:00

173 lines
5.4 KiB
Python

"""Portable Agent package DTOs and workspace-sensitive value filtering."""
from __future__ import annotations
import hashlib
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
from models.agent import Agent
from models.agent_config_entities import AgentSoulConfig
AGENT_PACKAGE_SCHEMA_VERSION = 1
AGENT_PACKAGE_REF_KEY = "package_ref"
AGENT_NODE_JOB_DSL_KEY = "agent_job"
class AgentPackageMetadata(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str = Field(min_length=1, max_length=255)
description: str = ""
role: str = ""
icon_type: str | None = None
icon: str | None = None
icon_background: str | None = None
class AgentPackageOmittedAsset(BaseModel):
model_config = ConfigDict(extra="forbid")
kind: Literal["skill", "file"]
name: str
size: int | None = None
hash: str | None = None
mime_type: str | None = None
class AgentPackageWorkspaceSkill(BaseModel):
"""Workspace Skill binding represented without source-workspace identifiers."""
model_config = ConfigDict(extra="forbid")
name: str = Field(min_length=1, max_length=64)
display_name: str = ""
description: str = ""
priority: int = Field(ge=0)
class AgentPackage(BaseModel):
"""One portable Agent Soul with display metadata and omitted asset hints."""
model_config = ConfigDict(extra="forbid")
schema_version: Literal[1] = 1
metadata: AgentPackageMetadata
soul: AgentSoulConfig
omitted_assets: list[AgentPackageOmittedAsset] = Field(default_factory=list)
workspace_skills: list[AgentPackageWorkspaceSkill] = Field(default_factory=list)
def portable_ref(prefix: str, value: str) -> str:
digest = hashlib.sha256(value.encode()).hexdigest()[:16]
return f"{prefix}-{digest}"
def _strip_sensitive_values(value: Any) -> Any:
"""Remove nested credential, secret, and uploaded-file locators."""
if isinstance(value, list):
return [_strip_sensitive_values(item) for item in value]
if not isinstance(value, dict):
return value
result: dict[str, Any] = {}
for key, item in value.items():
normalized_key = key.lower()
is_sensitive = (
"credential" in normalized_key
or "secret" in normalized_key
or "password" in normalized_key
or normalized_key in {"api_key", "token", "access_token", "refresh_token"}
or normalized_key.endswith("file_id")
or normalized_key == "upload_file_id"
)
result[key] = None if is_sensitive else _strip_sensitive_values(item)
return result
def make_portable_agent_package(
agent: Agent,
agent_soul: AgentSoulConfig,
workspace_skills: list[AgentPackageWorkspaceSkill] | None = None,
) -> AgentPackage:
"""Return a package safe to place in YAML or the system clipboard."""
soul_data = agent_soul.model_dump(mode="json")
omitted_assets = [
AgentPackageOmittedAsset(
kind="skill",
name=item.name,
size=item.size,
hash=item.hash,
mime_type=item.mime_type,
)
for item in agent_soul.config_skills
]
omitted_assets.extend(
AgentPackageOmittedAsset(
kind="file",
name=item.name,
size=item.size,
hash=item.hash,
mime_type=item.mime_type,
)
for item in agent_soul.config_files
)
for item in soul_data.get("config_skills", []):
item["file_id"] = ""
item["is_missing"] = True
for item in soul_data.get("config_files", []):
item["file_id"] = ""
item["is_missing"] = True
if soul_data.get("model"):
soul_data["model"]["credential_ref"] = None
for tool in soul_data.get("tools", {}).get("dify_tools", []):
tool["credential_type"] = "unauthorized"
tool["credential_ref"] = None
tool["runtime_parameters"] = _strip_sensitive_values(tool.get("runtime_parameters", {}))
for tool in soul_data.get("tools", {}).get("cli_tools", []):
env = tool.get("env") or {}
env["secret_refs"] = [
{
key: secret_ref.get(key)
for key in ("name", "key", "env_name", "variable", "type", "provider")
if secret_ref.get(key) is not None
}
for secret_ref in env.get("secret_refs", [])
]
tool["env"] = env
soul_data.setdefault("env", {})["secret_refs"] = [
{
key: secret_ref.get(key)
for key in ("name", "key", "env_name", "variable", "type", "provider")
if secret_ref.get(key) is not None
}
for secret_ref in soul_data.get("env", {}).get("secret_refs", [])
]
for contact in soul_data.get("human", {}).get("contacts", []):
for key in ("id", "contact_id", "human_id", "tenant_id"):
contact[key] = None
portable_soul = AgentSoulConfig.model_validate(soul_data)
icon_type = agent.icon_type.value if agent.icon_type is not None else None
return AgentPackage(
metadata=AgentPackageMetadata(
name=agent.name,
description=agent.description or "",
role=agent.role or "",
icon_type=icon_type,
icon=agent.icon,
icon_background=agent.icon_background,
),
soul=portable_soul,
omitted_assets=omitted_assets,
workspace_skills=workspace_skills or [],
)