feat(agent-v2): sync nightly updates to main (#37599)

Co-authored-by: Jingyi-Dify <jingyi.qi@dify.ai>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: Bond Zhu <783504079@qq.com>
Co-authored-by: Yansong Zhang <916125788@qq.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
盐粒 Yanli
2026-06-18 05:03:34 +00:00
committed by GitHub
co-authored by Jingyi-Dify yyh Joel autofix-ci[bot] hjlarry Bond Zhu Yansong Zhang yyh
parent 79ab6c2ecd
commit 3f2d22ec0f
336 changed files with 26946 additions and 1282 deletions
+63 -28
View File
@@ -2,20 +2,28 @@ from uuid import UUID
from flask import abort, request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from pydantic import AliasChoices, BaseModel, Field, field_validator
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.app.app import (
AppDetailWithSite,
AppDetailWithSite as GenericAppDetailWithSite,
)
from controllers.console.app.app import (
AppListQuery,
AppPagination,
AppPartial,
CopyAppPayload,
UpdateAppPayload,
_normalize_app_list_query_args,
)
from controllers.console.app.app import (
AppPagination as GenericAppPagination,
)
from controllers.console.app.app import (
AppPartial as GenericAppPartial,
)
from controllers.console.app.app import (
UpdateAppPayload as GenericUpdateAppPayload,
)
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
@@ -80,7 +88,10 @@ class AgentAppCreatePayload(BaseModel):
return role
class AgentAppUpdatePayload(UpdateAppPayload):
# Keep agent-app roster DTOs agent-specific instead of reusing the shared
# /apps response/request models. The roster surface needs Agent-only fields such
# as `role`, while the generic console/apps contracts must stay unchanged.
class AgentAppUpdatePayload(GenericUpdateAppPayload):
role: str = Field(..., min_length=1, description="Agent role", max_length=255)
@field_validator("role")
@@ -100,19 +111,6 @@ class AgentAppPublishedReferenceResponse(BaseModel):
app_icon_background: str | None = None
class AgentAppPartial(AppPartial):
published_reference_count: int = 0
published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list)
class AgentAppPagination(BaseModel):
page: int
limit: int
total: int
has_more: bool
data: list[AgentAppPartial]
class AgentLogsQuery(BaseModel):
page: int = Field(default=1, ge=1, description="Page number")
limit: int = Field(default=20, ge=1, le=100, description="Page size")
@@ -149,6 +147,26 @@ class AgentStatisticsQuery(BaseModel):
return value
class AgentAppPartial(GenericAppPartial):
app_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
published_reference_count: int = 0
published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list)
class AgentAppDetailWithSite(GenericAppDetailWithSite):
app_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
class AgentAppPagination(GenericAppPagination):
data: list[AgentAppPartial] = Field( # type: ignore[assignment] # pyrefly: ignore[bad-override-mutable-attribute]
validation_alias=AliasChoices("items", "data")
)
register_schema_models(
console_ns,
AgentAppCreatePayload,
@@ -159,14 +177,14 @@ register_schema_models(
AgentStatisticsQuery,
AgentIdPath,
AppListQuery,
UpdateAppPayload,
RosterListQuery,
)
register_response_schema_models(
console_ns,
AppDetailWithSite,
AgentAppPagination,
AgentAppPublishedReferenceResponse,
AgentAppDetailWithSite,
AgentAppPartial,
AgentConfigSnapshotDetailResponse,
AgentConfigSnapshotListResponse,
AgentInviteOptionsResponse,
@@ -184,16 +202,25 @@ def _agent_roster_service() -> AgentRosterService:
def _serialize_agent_app_detail(app_model) -> dict:
"""Serialize an Agent App detail using roster-only DTOs.
`/agent` responses are roster-shaped rather than raw app-shaped: `id`
becomes the backing roster Agent id, `app_id` carries the underlying App
id, and `role` is injected from the backing roster Agent. Keeping that
remap in this serializer lets generated console/agent contracts expose the
roster persona fields without widening the shared /apps detail schema.
"""
app_model = AppService().get_app(app_model)
if FeatureService.get_system_features().webapp_auth.enabled:
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
roster_service = _agent_roster_service()
agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id)
payload = AgentAppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=str(app_model.id))
if not agent:
raise AgentNotFoundError()
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
payload.pop("bound_agent_id", None)
payload["app_id"] = str(app_model.id)
payload["id"] = agent.id
@@ -206,6 +233,14 @@ def _serialize_agent_app_detail(app_model) -> dict:
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
"""Serialize Agent App lists with roster-shaped items.
Each item starts from the shared App list shape, then drops
`bound_agent_id`, rewrites `id` to the backing roster Agent id, stores the
original App id in `app_id`, and injects roster-only `role` when a backing
Agent is present.
"""
app_ids = [str(app.id) for app in app_pagination.items]
roster_service = _agent_roster_service()
agents_by_app_id = roster_service.load_app_backing_agents_by_app_id(
@@ -220,7 +255,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
tenant_id=tenant_id,
agent_ids=[agent.id for agent in agents_by_app_id.values()],
)
payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
payload = AgentAppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
for item in payload["data"]:
app_id = item["id"]
item.pop("bound_agent_id", None)
@@ -294,7 +329,7 @@ class AgentAppListApi(Resource):
return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id)
@console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__])
@console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(201, "Agent app created successfully", console_ns.models[AgentAppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@setup_required
@@ -322,7 +357,7 @@ class AgentAppListApi(Resource):
@console_ns.route("/agent/<uuid:agent_id>")
class AgentAppApi(Resource):
@console_ns.response(200, "Agent app detail", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(200, "Agent app detail", console_ns.models[AgentAppDetailWithSite.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -333,7 +368,7 @@ class AgentAppApi(Resource):
return _serialize_agent_app_detail(app_model)
@console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__])
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AgentAppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@setup_required
@@ -373,7 +408,7 @@ class AgentAppApi(Resource):
@console_ns.route("/agent/<uuid:agent_id>/copy")
class AgentAppCopyApi(Resource):
@console_ns.expect(console_ns.models[CopyAppPayload.__name__])
@console_ns.response(201, "Agent app copied successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(201, "Agent app copied successfully", console_ns.models[AgentAppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@setup_required
+29 -88
View File
@@ -30,7 +30,7 @@ from models import Account
from models.agent_config_entities import AgentFileRefConfig, AgentSkillRefConfig
from models.model import App, AppMode, UploadFile
from services.agent.composer_service import AgentComposerService
from services.agent.skill_package_service import SkillManifest, SkillPackageError, SkillPackageService
from services.agent.skill_package_service import SkillManifest, SkillPackageError
from services.agent.skill_standardize_service import SkillStandardizeService
from services.agent.skill_tool_inference_service import (
SkillToolInferenceError,
@@ -45,11 +45,18 @@ from services.agent_drive_service import (
normalize_drive_key,
)
from services.agent_service import AgentService
from services.file_service import FileService
logger = logging.getLogger(__name__)
_WORKFLOW_AGENT_DRIVE_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]
_AGENT_SKILL_UPLOAD_PARAMS = {
"file": {
"in": "formData",
"type": "file",
"required": True,
"description": "Skill package (.zip or .skill).",
}
}
class AgentLogQuery(BaseModel):
@@ -125,11 +132,6 @@ class AgentSkillUploadResponse(ResponseModel):
manifest: SkillManifest
class AgentSkillStandardizeResponse(ResponseModel):
skill: AgentSkillRefConfig
manifest: SkillManifest
class AgentDriveFileResponse(ResponseModel):
name: str
drive_key: str
@@ -156,7 +158,6 @@ register_response_schema_models(
AgentDriveFileCommitResponse,
AgentDriveFileResponse,
AgentLogResponse,
AgentSkillStandardizeResponse,
AgentSkillUploadResponse,
SkillToolInferenceResult,
)
@@ -174,30 +175,9 @@ def _agent_not_bound() -> tuple[dict[str, str], int]:
return {"code": "agent_not_bound", "message": "no agent is bound for this app/node"}, 400
def _upload_skill_for_app(*, current_user: Account):
if "file" not in request.files:
return {"code": "no_file", "message": "no skill file uploaded"}, 400
if len(request.files) > 1:
return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400
def _upload_skill_for_app(*, current_user: Account, app_model: App):
"""Upload one skill package and commit its normalized files into the agent drive."""
upload = request.files["file"]
content = upload.stream.read()
try:
manifest = SkillPackageService().validate_and_extract(content=content, filename=upload.filename or "")
except SkillPackageError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
upload_file = FileService(db.engine).upload_file(
filename=upload.filename or "skill.zip",
content=content,
mimetype=upload.mimetype or "application/zip",
user=current_user,
)
skill_ref = manifest.to_skill_ref(file_id=upload_file.id)
return {"skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump()}, 201
def _standardize_skill_for_app(*, current_user: Account, app_model: App):
query = query_params_from_request(AgentDriveMutationQuery)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
@@ -382,51 +362,9 @@ class AgentLogApi(Resource):
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
class AgentSkillUploadByAgentApi(Resource):
@console_ns.doc("upload_agent_skill_by_agent")
@console_ns.doc(description="Upload + validate a Skill package for an Agent App")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__])
@console_ns.response(400, "Invalid skill package")
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _upload_skill_for_app(current_user=current_user)
@console_ns.route("/apps/<uuid:app_id>/agent/skills/upload")
class AgentSkillUploadApi(Resource):
@console_ns.doc("upload_agent_skill")
@console_ns.doc(description="Upload + validate a Skill package (.zip/.skill) and extract its manifest")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__])
@console_ns.response(400, "Invalid skill package")
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user
def post(self, current_user: Account, app_model: App):
"""Validate an uploaded Skill package and persist the archive.
Returns a validated skill ref (to bind into the Agent soul config on save)
plus its manifest. Standardizing into the agent drive is ENG-594.
"""
return _upload_skill_for_app(current_user=current_user)
@console_ns.route("/agent/<uuid:agent_id>/skills/standardize")
class AgentSkillStandardizeByAgentApi(Resource):
@console_ns.doc("standardize_agent_skill_by_agent")
@console_ns.doc(description="Validate + standardize a Skill into an Agent App drive")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.response(
201,
"Skill standardized into drive",
console_ns.models[AgentSkillStandardizeResponse.__name__],
)
@console_ns.doc(description="Upload + standardize a Skill into an Agent App drive")
@console_ns.doc(consumes=["multipart/form-data"], params={"agent_id": "Agent ID", **_AGENT_SKILL_UPLOAD_PARAMS})
@console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__])
@console_ns.response(400, "Invalid skill package or no bound agent")
@setup_required
@login_required
@@ -435,19 +373,22 @@ class AgentSkillStandardizeByAgentApi(Resource):
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _standardize_skill_for_app(current_user=current_user, app_model=app_model)
return _upload_skill_for_app(current_user=current_user, app_model=app_model)
@console_ns.route("/apps/<uuid:app_id>/agent/skills/standardize")
class AgentSkillStandardizeApi(Resource):
@console_ns.doc("standardize_agent_skill")
@console_ns.doc(description="Validate + standardize a Skill into the agent drive (ENG-594)")
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentDriveMutationQuery)})
@console_ns.response(
201,
"Skill standardized into drive",
console_ns.models[AgentSkillStandardizeResponse.__name__],
@console_ns.route("/apps/<uuid:app_id>/agent/skills/upload")
class AgentSkillUploadApi(Resource):
@console_ns.doc("upload_agent_skill")
@console_ns.doc(description="Upload + standardize a Skill into the agent drive")
@console_ns.doc(
consumes=["multipart/form-data"],
params={
"app_id": "Application ID",
**query_params_from_model(AgentDriveMutationQuery),
**_AGENT_SKILL_UPLOAD_PARAMS,
},
)
@console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__])
@console_ns.response(400, "Invalid skill package or no bound agent")
@setup_required
@login_required
@@ -455,8 +396,8 @@ class AgentSkillStandardizeApi(Resource):
@get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user
def post(self, current_user: Account, app_model: App):
"""Upload a Skill, validate it, and standardize it into the app agent's drive."""
return _standardize_skill_for_app(current_user=current_user, app_model=app_model)
"""Upload a Skill, validate it, and commit drive-backed skill files."""
return _upload_skill_for_app(current_user=current_user, app_model=app_model)
@console_ns.route("/agent/<uuid:agent_id>/files")
+7 -7
View File
@@ -402,8 +402,6 @@ class AppPartial(ResponseModel):
bound_agent_id: str | None = None
# For Agent App responses exposed through /agent.
app_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
is_starred: bool = False
@computed_field(return_type=str | None) # type: ignore
@@ -457,8 +455,6 @@ class AppDetailWithSite(AppDetail):
bound_agent_id: str | None = None
# For Agent App responses exposed through /agent.
app_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
@computed_field(return_type=str | None) # type: ignore
@property
@@ -541,10 +537,7 @@ register_schema_models(
ModelConfig,
Site,
DeletedTool,
AppPartial,
AppDetail,
AppDetailWithSite,
AppPagination,
AppExportResponse,
Segmentation,
PreProcessingRule,
@@ -564,6 +557,13 @@ register_schema_models(
LoadBalancingPayload,
)
register_response_schema_models(
console_ns,
AppPartial,
AppDetailWithSite,
AppPagination,
)
@console_ns.route("/apps")
class AppListApi(Resource):