diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index fd725a75a1f..d16b5cc77cb 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -297,7 +297,15 @@ def _delete_drive_file_for_app(*, current_user: Account, app_model: App, allow_n except AgentDriveError as exc: return {"code": exc.code, "message": exc.message}, exc.status_code - result = [{"key": key, "removed": True}] + try: + result = AgentDriveService().commit( + tenant_id=app_model.tenant_id, + user_id=current_user.id, + agent_id=agent_id, + items=[DriveCommitItem(key=key, file_ref=None)], + ) + except AgentDriveError as exc: + return {"code": exc.code, "message": exc.message}, exc.status_code _sync_active_soul_files( tenant_id=app_model.tenant_id, agent_id=agent_id, @@ -318,33 +326,24 @@ def _delete_skill_for_app(*, current_user: Account, app_model: App, slug: str, a return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400 try: - agent_soul = AgentSoulFilesService.active_agent_soul( - session=db.session, + result = AgentDriveService().commit( tenant_id=app_model.tenant_id, + user_id=current_user.id, agent_id=agent_id, + items=[ + DriveCommitItem(key=f"{slug}/SKILL.md", file_ref=None), + DriveCommitItem(key=f"{slug}/.DIFY-SKILL-FULL.zip", file_ref=None), + ], ) except AgentDriveError as exc: return {"code": exc.code, "message": exc.message}, exc.status_code - skill_prefix = f"{slug}/" - removed_keys = [ - file_ref.drive_key - for skill in agent_soul.files.skills - if ( - skill.path or (AgentSoulFilesService.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else "") - ).strip("/") - == slug - for file_ref in skill.file_refs - if file_ref.drive_key - ] - if f"{slug}/SKILL.md" not in removed_keys: - removed_keys.append(f"{slug}/SKILL.md") - result = [{"key": key, "removed": True} for key in removed_keys if key.startswith(skill_prefix)] _sync_active_soul_files( tenant_id=app_model.tenant_id, agent_id=agent_id, account_id=current_user.id, committed_items=result, ) + removed_keys = [item["key"] for item in result if item.get("removed")] return {"result": "success", "removed_keys": removed_keys} diff --git a/api/controllers/console/app/agent_drive_inspector.py b/api/controllers/console/app/agent_drive_inspector.py index dc8a2d27d26..738b27750d5 100644 --- a/api/controllers/console/app/agent_drive_inspector.py +++ b/api/controllers/console/app/agent_drive_inspector.py @@ -31,7 +31,6 @@ from controllers.console.wraps import account_initialization_required, setup_req from extensions.ext_database import db from fields.base import ResponseModel from libs.login import login_required -from models.agent import AgentDriveFileKind from models.model import App, AppMode from services.agent.composer_service import AgentComposerService from services.agent.soul_files_service import AgentSoulFilesService @@ -168,12 +167,7 @@ def _versioned_manifest(*, tenant_id: str, agent_id: str, prefix: str = "") -> l normalized_prefix = prefix.strip().lstrip("/") skill_prefixes = AgentSoulFilesService.allowed_skill_prefixes(agent_soul) if normalized_prefix and any(normalized_prefix.startswith(p) for p in skill_prefixes): - return AgentSoulFilesService.list_manifest_items( - session=db.session, - tenant_id=tenant_id, - agent_id=agent_id, - prefix=normalized_prefix, - ) + return AgentDriveService().manifest(tenant_id=tenant_id, agent_id=agent_id, prefix=normalized_prefix) return AgentSoulFilesService.list_files( session=db.session, tenant_id=tenant_id, @@ -196,108 +190,6 @@ def _assert_key_in_active_soul(*, tenant_id: str, agent_id: str, key: str) -> No ) -def _file_ref_for_active_soul(*, tenant_id: str, agent_id: str, key: str): - agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id) - file_ref = AgentSoulFilesService.file_ref_for_key(agent_soul=agent_soul, key=key) - if file_ref is None and not AgentSoulFilesService.key_allowed_by_soul(agent_soul=agent_soul, key=key): - raise AgentDriveError( - "drive_key_not_in_agent_soul", - "drive key is not part of the active Agent Soul version", - status_code=404, - ) - return file_ref - - -def _archive_member_ref_for_active_soul( - *, tenant_id: str, agent_id: str, key: str -) -> tuple[AgentDriveFileKind, str, str] | None: - agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id) - normalized_key = key.strip().lstrip("/") - for skill in agent_soul.files.skills: - skill_path = skill.path or ( - AgentSoulFilesService.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else None - ) - if not skill_path or not normalized_key.startswith(f"{skill_path}/"): - continue - member_path = normalized_key.removeprefix(f"{skill_path}/") - if member_path == ".DIFY-SKILL-FULL.zip": - return None - manifest_files = {str(path).strip().lstrip("/") for path in (skill.manifest_files or [])} - if manifest_files and member_path not in manifest_files: - return None - archive_file_id = skill.full_archive_file_id - if not archive_file_id: - return None - return AgentDriveFileKind.TOOL_FILE, archive_file_id, member_path - return None - - -def _file_kind_from_ref(file_ref) -> AgentDriveFileKind | None: - raw = file_ref.transfer_method or ("upload_file" if file_ref.upload_file_id else None) - if raw is None: - return None - try: - return AgentDriveFileKind(raw) - except ValueError as exc: - raise AgentDriveError("invalid_drive_file_ref", "Agent Soul file ref has invalid transfer method") from exc - - -def _preview_versioned_file(*, tenant_id: str, agent_id: str, key: str) -> dict[str, Any]: - file_ref = _file_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key) - if file_ref is None: - archive_member = _archive_member_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key) - if archive_member is not None: - archive_file_kind, archive_file_id, member_path = archive_member - return AgentDriveService().preview_archive_member_for_ref( - tenant_id=tenant_id, - agent_id=agent_id, - key=key, - archive_file_kind=archive_file_kind, - archive_file_id=archive_file_id, - member_path=member_path, - ) - return AgentDriveService().preview(tenant_id=tenant_id, agent_id=agent_id, key=key) - file_kind = _file_kind_from_ref(file_ref) - file_id = file_ref.file_id or file_ref.upload_file_id - if file_kind is None or not file_id: - raise AgentDriveError("invalid_drive_file_ref", "Agent Soul file ref is missing file id", status_code=404) - return AgentDriveService().preview_file_ref( - tenant_id=tenant_id, - agent_id=agent_id, - key=key, - file_kind=file_kind, - file_id=file_id, - size=file_ref.get("size"), - ) - - -def _download_versioned_file(*, tenant_id: str, agent_id: str, key: str) -> str: - file_ref = _file_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key) - if file_ref is None: - archive_member = _archive_member_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key) - if archive_member is not None: - archive_file_kind, archive_file_id, member_path = archive_member - return AgentDriveService().download_url_archive_member_for_ref( - tenant_id=tenant_id, - agent_id=agent_id, - key=key, - archive_file_kind=archive_file_kind, - archive_file_id=archive_file_id, - member_path=member_path, - ) - return AgentDriveService().download_url(tenant_id=tenant_id, agent_id=agent_id, key=key) - file_kind = _file_kind_from_ref(file_ref) - file_id = file_ref.file_id or file_ref.upload_file_id - if file_kind is None or not file_id: - raise AgentDriveError("invalid_drive_file_ref", "Agent Soul file ref is missing file id", status_code=404) - return AgentDriveService().download_url_for_ref( - tenant_id=tenant_id, - agent_id=agent_id, - file_kind=file_kind, - file_id=file_id, - ) - - def _assert_skill_in_active_soul(*, tenant_id: str, agent_id: str, skill_path: str) -> None: agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id) wanted = skill_path.strip().strip("/") @@ -402,7 +294,8 @@ class AgentDrivePreviewByAgentApi(Resource): query = query_params_from_request(AgentDriveFileByAgentQuery) resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - return _preview_versioned_file(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) + _assert_key_in_active_soul(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) + return AgentDriveService().preview(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) except AgentDriveError as exc: return _handle(exc) @@ -421,7 +314,8 @@ class AgentDriveDownloadByAgentApi(Resource): query = query_params_from_request(AgentDriveFileByAgentQuery) resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - url = _download_versioned_file(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) + _assert_key_in_active_soul(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) + url = AgentDriveService().download_url(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) except AgentDriveError as exc: return _handle(exc) return {"url": url} @@ -523,7 +417,8 @@ class AgentDrivePreviewApi(Resource): if not agent_id: return _agent_not_bound() try: - return _preview_versioned_file(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key) + _assert_key_in_active_soul(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key) + return AgentDriveService().preview(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key) except AgentDriveError as exc: return _handle(exc) @@ -544,7 +439,8 @@ class AgentDriveDownloadApi(Resource): if not agent_id: return _agent_not_bound() try: - url = _download_versioned_file(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key) + _assert_key_in_active_soul(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key) + url = AgentDriveService().download_url(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key) except AgentDriveError as exc: return _handle(exc) return {"url": url} diff --git a/api/controllers/inner_api/plugin/agent_drive.py b/api/controllers/inner_api/plugin/agent_drive.py index 6047af28056..1177b6d8a1e 100644 --- a/api/controllers/inner_api/plugin/agent_drive.py +++ b/api/controllers/inner_api/plugin/agent_drive.py @@ -18,7 +18,6 @@ from controllers.inner_api import inner_api_ns from controllers.inner_api.plugin.wraps import get_user from controllers.inner_api.wraps import plugin_inner_api_only from extensions.ext_database import db -from models.agent import AgentDriveFileKind from services.agent.soul_files_service import AgentSoulFilesService from services.agent_drive_service import ( AgentDriveError, @@ -47,31 +46,17 @@ def _versioned_manifest( ) -> list[dict[str, object]]: agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id) normalized_prefix = prefix.strip().lstrip("/") - items = AgentSoulFilesService.list_manifest_items( - session=db.session, + items = AgentDriveService().manifest( tenant_id=tenant_id, agent_id=agent_id, prefix=normalized_prefix, + include_download_url=include_download_url, ) skill_prefixes = AgentSoulFilesService.allowed_skill_prefixes(agent_soul) - if normalized_prefix and not any(normalized_prefix.startswith(p) for p in skill_prefixes): - allowed_keys = AgentSoulFilesService.allowed_drive_keys(agent_soul) - items = [item for item in items if item.get("key") in allowed_keys] - if include_download_url: - for item in items: - file_kind = item.get("file_kind") - file_id = item.get("file_id") - if not file_kind or not file_id: - continue - try: - item["download_url"] = AgentDriveService.resolve_download_url_for_ref( - tenant_id=tenant_id, - file_kind=AgentDriveFileKind(str(file_kind)), - file_id=str(file_id), - ) - except ValueError: - item["download_url"] = None - return items + if normalized_prefix and any(normalized_prefix.startswith(p) for p in skill_prefixes): + return items + allowed_keys = AgentSoulFilesService.allowed_drive_keys(agent_soul) + return [item for item in items if item.get("key") in allowed_keys] @inner_api_ns.route("/drive//manifest") diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py index 46a1edefcf3..88ceddfdda7 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -72,7 +72,7 @@ from services.agent.prompt_mentions import ( parse_prompt_mentions, ) from services.agent.soul_files_service import AgentSoulFilesService -from services.agent_drive_service import decode_drive_mention_ref +from services.agent_drive_service import AgentDriveError, AgentDriveService, decode_drive_mention_ref from .output_failure_orchestrator import retry_idempotency_key from .plugin_tools_builder import WorkflowAgentPluginToolsBuilder, WorkflowAgentPluginToolsBuildError @@ -735,11 +735,12 @@ def build_drive_layer_config( for skill in agent_soul.files.skills if skill.skill_md_key ] - soul_file_keys = { - key - for key in AgentSoulFilesService.allowed_drive_keys(agent_soul) - if key not in {skill["skill_md_key"] for skill in skills_catalog} - } + soul_file_keys = {file_ref.drive_key for file_ref in agent_soul.files.files if file_ref.drive_key} + try: + manifest_items = AgentDriveService().manifest(tenant_id=tenant_id, agent_id=agent_id) + except AgentDriveError: + manifest_items = [] + manifest_by_key = {item["key"]: item for item in manifest_items} skill_keys = {skill["skill_md_key"] for skill in skills_catalog} warnings: list[dict[str, str]] = [] mentioned_skill_keys: list[str] = [] @@ -758,6 +759,15 @@ def build_drive_layer_config( "message": f"drive mention '{drive_key}' has no matching drive entry.", } ) + for drive_key in sorted(skill_keys | soul_file_keys): + if drive_key not in manifest_by_key: + warnings.append( + { + "section": "agent_soul.files", + "code": "drive_value_missing", + "message": f"Agent Soul drive ref '{drive_key}' has no backing drive value.", + } + ) skills = [ DifyDriveSkillConfig( diff --git a/api/models/agent_config_entities.py b/api/models/agent_config_entities.py index 2cdc33d63e2..e31d899fe37 100644 --- a/api/models/agent_config_entities.py +++ b/api/models/agent_config_entities.py @@ -160,10 +160,6 @@ class AgentSkillRefConfig(AgentFlexibleConfig): # Zip member path listing from standardization (ENG-371): lets infer-tools # show the model strong signals like ``scripts/*.sh`` without unpacking. manifest_files: list[str] | None = None - # Versioned drive KV entries that belong to this skill package. The drive - # table remains the storage index, but Agent Soul owns which concrete file - # ids are visible for a snapshot/version. - file_refs: list[AgentFileRefConfig] = Field(default_factory=list) class AgentSoulFilesConfig(BaseModel): diff --git a/api/services/agent/soul_files_service.py b/api/services/agent/soul_files_service.py index 3f63ce078a3..2de16337352 100644 --- a/api/services/agent/soul_files_service.py +++ b/api/services/agent/soul_files_service.py @@ -106,35 +106,6 @@ class AgentSoulFilesService: items.append(item) return items - @classmethod - def list_manifest_items( - cls, - *, - session: Session, - tenant_id: str, - agent_id: str, - prefix: str = "", - ) -> list[dict[str, Any]]: - agent_soul = cls.active_agent_soul(session=session, tenant_id=tenant_id, agent_id=agent_id) - refs = cls._all_file_refs(agent_soul) - if prefix: - normalized_prefix = normalize_drive_key(prefix) - refs = [ref for ref in refs if (ref.drive_key or "").startswith(normalized_prefix)] - keys = [ref.drive_key for ref in refs if ref.drive_key] - rows = cls._drive_rows_by_key(session=session, tenant_id=tenant_id, agent_id=agent_id, keys=keys) - - items: list[dict[str, Any]] = [] - for file_ref in refs: - key = file_ref.drive_key - if not key: - continue - row = rows.get(key) - item = cls._file_item_from_ref(file_ref) - item.update(cls._row_item(row) if row is not None else {"key": key, "missing": True}) - item["file_id"] = file_ref.file_id or file_ref.upload_file_id - items.append(item) - return sorted(items, key=lambda item: str(item.get("key") or "")) - @classmethod def list_skills( cls, @@ -155,16 +126,15 @@ class AgentSoulFilesService: continue row = rows.get(skill.skill_md_key) archive_key = skill.full_archive_key if skill.full_archive_key in rows else None - skill_md_ref = cls.file_ref_for_key(agent_soul=agent_soul, key=skill.skill_md_key) items.append( { "path": skill.path or cls.skill_path_from_key(skill.skill_md_key), "skill_md_key": skill.skill_md_key, - "archive_key": archive_key or skill.full_archive_key, + "archive_key": archive_key, "name": skill.name, "description": skill.description, "size": row.size if row is not None else None, - "mime_type": row.mime_type if row is not None else (skill_md_ref.type if skill_md_ref else None), + "mime_type": row.mime_type if row is not None else None, "hash": row.hash if row is not None else None, "created_at": int(row.created_at.timestamp()) if row is not None and row.created_at else None, "missing": row is None, @@ -183,9 +153,6 @@ class AgentSoulFilesService: keys.add(skill.skill_md_key) if skill.full_archive_key: keys.add(skill.full_archive_key) - for file_ref in skill.file_refs: - if file_ref.drive_key: - keys.add(file_ref.drive_key) return keys @classmethod @@ -204,18 +171,6 @@ class AgentSoulFilesService: return True return any(normalized_key.startswith(prefix) for prefix in cls.allowed_skill_prefixes(agent_soul)) - @classmethod - def file_ref_for_key(cls, *, agent_soul: AgentSoulConfig, key: str) -> AgentFileRefConfig | None: - normalized_key = normalize_drive_key(key) - for file_ref in agent_soul.files.files: - if file_ref.drive_key == normalized_key: - return file_ref - for skill in agent_soul.files.skills: - for file_ref in skill.file_refs: - if file_ref.drive_key == normalized_key: - return file_ref - return None - @classmethod def drive_copy_scopes(cls, *, agent_soul: AgentSoulConfig) -> tuple[set[str], set[str]]: exact_keys = cls.allowed_drive_keys(agent_soul) @@ -268,8 +223,6 @@ class AgentSoulFilesService: return if key.startswith(_FILES_PREFIX): cls._upsert_file_ref(agent_soul=agent_soul, key=key, item=item) - return - cls._upsert_skill_file_ref(agent_soul=agent_soul, key=key, item=item) @classmethod def _upsert_skill_ref(cls, *, agent_soul: AgentSoulConfig, key: str, item: dict[str, Any]) -> None: @@ -286,18 +239,6 @@ class AgentSoulFilesService: full_archive_key=cls.skill_archive_key(key), manifest_files=metadata.manifest_files, ) - existing_ref = next( - (existing for existing in agent_soul.files.skills if existing.skill_md_key == key or existing.path == path), - None, - ) - file_refs = list(existing_ref.file_refs) if existing_ref else [] - file_refs = [file_ref for file_ref in file_refs if file_ref.drive_key != key] - file_refs.append(cls._file_ref_from_item(key=key, item=item, name="SKILL.md")) - archive_key = cls.skill_archive_key(key) - archive_ref = next((file_ref for file_ref in file_refs if file_ref.drive_key == archive_key), None) - if archive_ref: - ref.full_archive_file_id = archive_ref.file_id - ref.file_refs = sorted(file_refs, key=lambda value: value.drive_key or value.name) skills = [ existing for existing in agent_soul.files.skills if existing.skill_md_key != key and existing.path != path ] @@ -317,59 +258,12 @@ class AgentSoulFilesService: type=str(item.get("mime_type") or ""), transfer_method=str(item.get("file_kind") or ""), drive_key=key, - size=item.get("size"), - hash=item.get("hash"), ) files = [existing for existing in agent_soul.files.files if existing.drive_key != key] files.append(ref) files.sort(key=lambda value: value.drive_key or value.name) agent_soul.files.files = files - @classmethod - def _upsert_skill_file_ref(cls, *, agent_soul: AgentSoulConfig, key: str, item: dict[str, Any]) -> None: - path = key.split("/", 1)[0] - if not path: - return - updated: list[AgentSkillRefConfig] = [] - changed = False - for skill in agent_soul.files.skills: - skill_path = skill.path or (cls.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else "") - if skill_path != path: - updated.append(skill) - continue - file_ref = cls._file_ref_from_item(key=key, item=item) - file_refs = [existing for existing in skill.file_refs if existing.drive_key != key] - file_refs.append(file_ref) - replacement = skill.model_copy( - update={"file_refs": sorted(file_refs, key=lambda value: value.drive_key or value.name)} - ) - if key.endswith(f"/{_SKILL_ARCHIVE_NAME}"): - replacement = replacement.model_copy( - update={ - "full_archive_key": key, - "full_archive_file_id": file_ref.file_id, - } - ) - updated.append(replacement) - changed = True - if changed: - agent_soul.files.skills = updated - - @staticmethod - def _file_ref_from_item(*, key: str, item: dict[str, Any], name: str | None = None) -> AgentFileRefConfig: - file_id = str(item.get("file_id") or "") - return AgentFileRefConfig( - id=key, - file_id=file_id, - upload_file_id=file_id if item.get("file_kind") == "upload_file" else None, - name=name or key.rsplit("/", 1)[-1], - type=str(item.get("mime_type") or ""), - transfer_method=str(item.get("file_kind") or ""), - drive_key=key, - size=item.get("size"), - hash=item.get("hash"), - ) - @classmethod def _remove_ref(cls, *, agent_soul: AgentSoulConfig, key: str) -> None: agent_soul.files.files = [file_ref for file_ref in agent_soul.files.files if file_ref.drive_key != key] @@ -381,35 +275,11 @@ class AgentSoulFilesService: return if key.endswith(f"/{_SKILL_ARCHIVE_NAME}"): agent_soul.files.skills = [ - skill.model_copy( - update={ - "full_archive_key": None, - "full_archive_file_id": None, - "file_refs": [file_ref for file_ref in skill.file_refs if file_ref.drive_key != key], - } - ) + skill.model_copy(update={"full_archive_key": None}) if skill.full_archive_key == key else skill for skill in agent_soul.files.skills ] - return - path = key.split("/", 1)[0] - if path: - agent_soul.files.skills = [ - skill.model_copy( - update={"file_refs": [file_ref for file_ref in skill.file_refs if file_ref.drive_key != key]} - ) - if (skill.path or (cls.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else "")) == path - else skill - for skill in agent_soul.files.skills - ] - - @staticmethod - def _all_file_refs(agent_soul: AgentSoulConfig) -> list[AgentFileRefConfig]: - refs = list(agent_soul.files.files) - for skill in agent_soul.files.skills: - refs.extend(skill.file_refs) - return refs @staticmethod def _parse_skill_metadata(raw_metadata: Any) -> DriveSkillMetadata: @@ -464,8 +334,6 @@ class AgentSoulFilesService: "mime_type": file_ref.type, "file_kind": file_ref.transfer_method, "is_skill": False, - "size": file_ref.get("size"), - "hash": file_ref.get("hash"), } @classmethod diff --git a/api/services/agent/workflow_publish_service.py b/api/services/agent/workflow_publish_service.py index 8b6f76d8b10..9d60b9dd63d 100644 --- a/api/services/agent/workflow_publish_service.py +++ b/api/services/agent/workflow_publish_service.py @@ -13,7 +13,7 @@ from models import ToolFile, UploadFile from models.agent import ( Agent, AgentConfigSnapshot, - AgentDriveFileKind, + AgentDriveFile, AgentScope, AgentStatus, WorkflowAgentBindingType, @@ -184,7 +184,7 @@ class WorkflowAgentPublishService: wanted_keys: dict[str, tuple[str, str]] = {} soul_skill_keys = {skill.skill_md_key for skill in agent_soul.files.skills if skill.skill_md_key} - soul_file_keys = AgentSoulFilesService.allowed_drive_keys(agent_soul=agent_soul) - soul_skill_keys + soul_file_keys = {file_ref.drive_key for file_ref in agent_soul.files.files if file_ref.drive_key} for mention in parse_prompt_mentions(agent_soul.prompt.system_prompt): if mention.kind not in {MentionKind.SKILL, MentionKind.FILE}: continue @@ -200,11 +200,15 @@ class WorkflowAgentPublishService: check_keys = sorted(set(wanted_keys) | declared_keys) if not check_keys: return - existing_keys = { - key - for key in check_keys - if cls._soul_file_ref_exists(session=session, tenant_id=binding.tenant_id, agent_soul=agent_soul, key=key) - } + existing_keys = set( + session.scalars( + select(AgentDriveFile.key).where( + AgentDriveFile.tenant_id == binding.tenant_id, + AgentDriveFile.agent_id == binding.agent_id, + AgentDriveFile.key.in_(check_keys), + ) + ).all() + ) messages: list[str] = [] for key, (code, display) in wanted_keys.items(): if code == "skill_ref_dangling" and key not in soul_skill_keys: @@ -225,35 +229,6 @@ class WorkflowAgentPublishService: f"Workflow Agent node {binding.node_id} has invalid Agent Soul drive refs: {'; '.join(messages)}" ) - @staticmethod - def _soul_file_ref_exists( - *, - session: Session, - tenant_id: str, - agent_soul: AgentSoulConfig, - key: str, - ) -> bool: - file_ref = AgentSoulFilesService.file_ref_for_key(agent_soul=agent_soul, key=key) - if file_ref is None: - return False - file_id = file_ref.file_id or file_ref.upload_file_id - if not file_id: - return False - raw_kind = file_ref.transfer_method or ("upload_file" if file_ref.upload_file_id else None) - try: - file_kind = AgentDriveFileKind(str(raw_kind)) - except ValueError: - return False - if file_kind == AgentDriveFileKind.TOOL_FILE: - return ( - session.scalar(select(ToolFile.id).where(ToolFile.tenant_id == tenant_id, ToolFile.id == file_id)) - is not None - ) - return ( - session.scalar(select(UploadFile.id).where(UploadFile.tenant_id == tenant_id, UploadFile.id == file_id)) - is not None - ) - @classmethod def sync_agent_bindings_for_draft( cls, diff --git a/api/services/agent_drive_service.py b/api/services/agent_drive_service.py index d7888288363..9adf3c42580 100644 --- a/api/services/agent_drive_service.py +++ b/api/services/agent_drive_service.py @@ -838,24 +838,6 @@ class AgentDriveService: except ValueError: return None - @classmethod - def resolve_download_url_for_ref( - cls, - *, - tenant_id: str, - file_kind: AgentDriveFileKind, - file_id: str, - for_external: bool = False, - as_attachment: bool = False, - ) -> str | None: - return cls._resolve_download_url( - tenant_id=tenant_id, - file_kind=file_kind, - file_id=file_id, - for_external=for_external, - as_attachment=as_attachment, - ) - # ── console drive inspector (ENG-624) ──────────────────────────────────── # SKILL.md is the primary preview use case; 64 KiB covers it with headroom @@ -1035,33 +1017,6 @@ class AgentDriveService: break return self._preview_bytes(key=response_key, size=size, payload=bytes(data)) - def preview_file_ref( - self, - *, - tenant_id: str, - agent_id: str, - key: str, - file_kind: AgentDriveFileKind, - file_id: str, - size: int | None = None, - ) -> dict[str, Any]: - """Preview a concrete versioned file ref recorded in Agent Soul.""" - with session_factory.create_session() as session: - self._assert_agent_belongs_to_tenant(session, tenant_id=tenant_id, agent_id=agent_id) - storage_key = self._storage_key_for_ref( - session, - tenant_id=tenant_id, - file_kind=file_kind, - file_id=file_id, - ) - - data = bytearray() - for chunk in storage.load_stream(storage_key): - data.extend(chunk) - if len(data) > self.PREVIEW_MAX_BYTES: - break - return self._preview_bytes(key=key, size=size, payload=bytes(data)) - def preview_archive_member_for_ref( self, *, @@ -1140,27 +1095,6 @@ class AgentDriveService: as_attachment=True, ) - def download_url_for_ref( - self, - *, - tenant_id: str, - agent_id: str, - file_kind: AgentDriveFileKind, - file_id: str, - ) -> str: - with session_factory.create_session() as session: - self._assert_agent_belongs_to_tenant(session, tenant_id=tenant_id, agent_id=agent_id) - url = self._resolve_download_url( - tenant_id=tenant_id, - file_kind=file_kind, - file_id=file_id, - for_external=True, - as_attachment=True, - ) - if url is None: - raise AgentDriveError("drive_key_not_found", "drive value cannot be resolved", status_code=404) - return url - @staticmethod def _secret_key() -> bytes: return dify_config.SECRET_KEY.encode() @@ -1290,7 +1224,6 @@ class AgentDriveService: filename = normalize_drive_key(key).rsplit("/", 1)[-1] return payload, mime_type, filename - __all__ = [ "AgentDriveError", "AgentDriveService", diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index e7b32a55af8..baf316080fd 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -959,11 +959,41 @@ def _soul_with_drive_skill() -> AgentSoulConfig: def _mock_drive_catalog(monkeypatch: pytest.MonkeyPatch) -> None: - return None + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.list_skills", + lambda self, *, tenant_id, agent_id: [ + { + "path": "tender-analyzer", + "skill_md_key": "tender-analyzer/SKILL.md", + "archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", + "name": "Tender Analyzer", + "description": "Parses RFPs.", + "size": 123, + "mime_type": "text/markdown", + "hash": "hash-1", + "created_at": 1, + } + ], + ) + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.manifest", + lambda self, *, tenant_id, agent_id, prefix="", include_download_url=False: [ + {"key": "tender-analyzer/SKILL.md", "is_skill": True}, + {"key": "tender-analyzer/.DIFY-SKILL-FULL.zip", "is_skill": False}, + {"key": "files/sample.pdf", "is_skill": False}, + ], + ) def _mock_empty_drive_catalog(monkeypatch: pytest.MonkeyPatch) -> None: - return None + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.list_skills", + lambda self, *, tenant_id, agent_id: [], + ) + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.manifest", + lambda self, *, tenant_id, agent_id, prefix="", include_download_url=False: [], + ) def test_build_drive_layer_config_catalogs_drive_skills_and_mentions(monkeypatch: pytest.MonkeyPatch): @@ -1089,6 +1119,14 @@ def test_workflow_runtime_missing_drive_mentions_fall_back_to_label_then_decoded monkeypatch.setattr( "core.workflow.nodes.agent_v2.runtime_request_builder.dify_config.AGENT_DRIVE_MANIFEST_ENABLED", True ) + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.list_skills", + lambda self, *, tenant_id, agent_id: [], + ) + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.manifest", + lambda self, *, tenant_id, agent_id, prefix="", include_download_url=False: [], + ) context = _context() context.snapshot.config_snapshot = AgentSoulConfig( prompt={ diff --git a/api/tests/unit_tests/services/agent/test_agent_soul_files_service.py b/api/tests/unit_tests/services/agent/test_agent_soul_files_service.py index 57bd25e9a0d..8e181189390 100644 --- a/api/tests/unit_tests/services/agent/test_agent_soul_files_service.py +++ b/api/tests/unit_tests/services/agent/test_agent_soul_files_service.py @@ -33,26 +33,6 @@ def test_apply_drive_commit_records_skill_and_file_refs_in_agent_soul(): "is_skill": False, }, ) - AgentSoulFilesService._apply_commit_item( - agent_soul=soul, - item={ - "key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - "file_kind": "tool_file", - "file_id": "archive-file", - "mime_type": "application/zip", - "is_skill": False, - }, - ) - AgentSoulFilesService._apply_commit_item( - agent_soul=soul, - item={ - "key": "tender-analyzer/src/main.py", - "file_kind": "tool_file", - "file_id": "member-file", - "mime_type": "text/x-python", - "is_skill": False, - }, - ) assert [skill.model_dump(mode="json", exclude_none=True) for skill in soul.files.skills] == [ { @@ -64,34 +44,7 @@ def test_apply_drive_commit_records_skill_and_file_refs_in_agent_soul(): "skill_md_key": "tender-analyzer/SKILL.md", "skill_md_file_id": "skill-md-file", "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - "full_archive_file_id": "archive-file", "manifest_files": ["SKILL.md", "src/main.py"], - "file_refs": [ - { - "id": "tender-analyzer/.DIFY-SKILL-FULL.zip", - "file_id": "archive-file", - "name": ".DIFY-SKILL-FULL.zip", - "type": "application/zip", - "transfer_method": "tool_file", - "drive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - }, - { - "id": "tender-analyzer/SKILL.md", - "file_id": "skill-md-file", - "name": "SKILL.md", - "type": "", - "transfer_method": "tool_file", - "drive_key": "tender-analyzer/SKILL.md", - }, - { - "id": "tender-analyzer/src/main.py", - "file_id": "member-file", - "name": "main.py", - "type": "text/x-python", - "transfer_method": "tool_file", - "drive_key": "tender-analyzer/src/main.py", - }, - ], } ] assert [file_ref.model_dump(mode="json", exclude_none=True) for file_ref in soul.files.files] == [ @@ -118,23 +71,6 @@ def test_apply_drive_commit_removes_refs_without_touching_unrelated_entries(): "path": "tender-analyzer", "skill_md_key": "tender-analyzer/SKILL.md", "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - "file_refs": [ - { - "id": "tender-analyzer/SKILL.md", - "file_id": "skill-md-file", - "name": "SKILL.md", - "type": "", - "transfer_method": "tool_file", - "drive_key": "tender-analyzer/SKILL.md", - }, - { - "id": "tender-analyzer/src/main.py", - "file_id": "member-file", - "name": "main.py", - "transfer_method": "tool_file", - "drive_key": "tender-analyzer/src/main.py", - }, - ], } ], "files": [ @@ -180,32 +116,3 @@ def test_drive_copy_and_access_scopes_come_from_agent_soul_files(): assert prefixes == {"tender-analyzer/"} assert AgentSoulFilesService.key_allowed_by_soul(agent_soul=soul, key="tender-analyzer/src/main.py") is True assert AgentSoulFilesService.key_allowed_by_soul(agent_soul=soul, key="files/other.pdf") is False - - -def test_file_ref_for_key_resolves_skill_member_refs(): - soul = AgentSoulConfig.model_validate( - { - "files": { - "skills": [ - { - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - "file_refs": [ - { - "id": "tender-analyzer/src/main.py", - "file_id": "member-file", - "name": "main.py", - "transfer_method": "tool_file", - "drive_key": "tender-analyzer/src/main.py", - } - ], - } - ] - } - } - ) - - file_ref = AgentSoulFilesService.file_ref_for_key(agent_soul=soul, key="tender-analyzer/src/main.py") - - assert file_ref is not None - assert file_ref.file_id == "member-file"