mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 14:51:13 +08:00
Revert "feat(agent): version agent soul drive file refs"
This reverts commit a6a6bc322b.
This commit is contained in:
parent
3f653b2dfb
commit
3948133faf
@ -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}
|
||||
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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/<string:drive_ref>/manifest")
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user