diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index d16b5cc77cb..86a3c473547 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -38,7 +38,6 @@ from services.agent.skill_tool_inference_service import ( SkillToolInferenceResult, SkillToolInferenceService, ) -from services.agent.soul_files_service import AgentSoulFilesService from services.agent_drive_service import ( AgentDriveError, AgentDriveService, @@ -182,22 +181,6 @@ 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 _sync_active_soul_files( - *, - tenant_id: str, - agent_id: str, - account_id: str, - committed_items: list[dict[str, Any]], -) -> None: - AgentSoulFilesService.sync_drive_commit_to_active_soul( - tenant_id=tenant_id, - agent_id=agent_id, - account_id=account_id, - committed_items=committed_items, - ) - db.session.commit() - - def _upload_skill_for_app(*, current_user: Account, app_model: App): """Upload one skill package and commit its normalized files into the agent drive.""" @@ -212,9 +195,8 @@ def _upload_skill_for_app(*, current_user: Account, app_model: App): upload = request.files["file"] content = upload.stream.read() - standardize_service = SkillStandardizeService() try: - result = standardize_service.standardize( + result = SkillStandardizeService().standardize( content=content, filename=upload.filename or "", tenant_id=app_model.tenant_id, @@ -223,12 +205,6 @@ def _upload_skill_for_app(*, current_user: Account, app_model: App): ) except (SkillPackageError, 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, - account_id=current_user.id, - committed_items=standardize_service.last_committed_items, - ) return result, 201 @@ -267,12 +243,6 @@ def _commit_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 - _sync_active_soul_files( - tenant_id=app_model.tenant_id, - agent_id=agent_id, - account_id=current_user.id, - committed_items=committed, - ) row = committed[0] return { @@ -306,12 +276,6 @@ 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 - _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} @@ -337,12 +301,6 @@ def _delete_skill_for_app(*, current_user: Account, app_model: App, slug: str, a ) 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, - 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 738b27750d5..bd639955d9c 100644 --- a/api/controllers/console/app/agent_drive_inspector.py +++ b/api/controllers/console/app/agent_drive_inspector.py @@ -28,12 +28,10 @@ from controllers.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_app_model from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id -from extensions.ext_database import db from fields.base import ResponseModel from libs.login import login_required from models.model import App, AppMode from services.agent.composer_service import AgentComposerService -from services.agent.soul_files_service import AgentSoulFilesService from services.agent_drive_service import AgentDriveError, AgentDriveService @@ -162,50 +160,6 @@ def _handle(exc: AgentDriveError) -> tuple[dict[str, object], int]: return {"code": exc.code, "message": exc.message}, exc.status_code -def _versioned_manifest(*, tenant_id: str, agent_id: str, prefix: str = "") -> list[dict[str, Any]]: - agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id) - 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 AgentDriveService().manifest(tenant_id=tenant_id, agent_id=agent_id, prefix=normalized_prefix) - return AgentSoulFilesService.list_files( - session=db.session, - tenant_id=tenant_id, - agent_id=agent_id, - prefix=normalized_prefix, - ) - - -def _versioned_skills(*, tenant_id: str, agent_id: str) -> list[dict[str, Any]]: - return AgentSoulFilesService.list_skills(session=db.session, tenant_id=tenant_id, agent_id=agent_id) - - -def _assert_key_in_active_soul(*, tenant_id: str, agent_id: str, key: str) -> None: - agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id) - if 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, - ) - - -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("/") - for skill in agent_soul.files.skills: - path = skill.path - if not path and skill.skill_md_key: - path = AgentSoulFilesService.skill_path_from_key(skill.skill_md_key) - if path == wanted: - return - raise AgentDriveError( - "skill_not_in_agent_soul", - "skill is not part of the active Agent Soul version", - status_code=404, - ) - - def _json_response(data: Mapping[str, Any]): return Response( response=json.dumps(data, ensure_ascii=False, separators=(",", ":")), @@ -230,7 +184,7 @@ class AgentDriveListByAgentApi(Resource): query = query_params_from_request(AgentDriveListByAgentQuery) resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - items = _versioned_manifest(tenant_id=tenant_id, agent_id=str(agent_id), prefix=query.prefix) + items = AgentDriveService().manifest(tenant_id=tenant_id, agent_id=str(agent_id), prefix=query.prefix) except AgentDriveError as exc: return _handle(exc) return {"items": [{k: v for k, v in item.items() if k != "file_id"} for item in items]} @@ -249,7 +203,7 @@ class AgentDriveSkillListByAgentApi(Resource): def get(self, tenant_id: str, agent_id: UUID): resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - items = _versioned_skills(tenant_id=tenant_id, agent_id=str(agent_id)) + items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=str(agent_id)) except AgentDriveError as exc: return _handle(exc) return {"items": items} @@ -268,7 +222,6 @@ class AgentDriveSkillInspectByAgentApi(Resource): def get(self, tenant_id: str, agent_id: UUID, skill_path: str): resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - _assert_skill_in_active_soul(tenant_id=tenant_id, agent_id=str(agent_id), skill_path=skill_path) return _json_response( AgentDriveService().inspect_skill( tenant_id=tenant_id, @@ -294,7 +247,6 @@ class AgentDrivePreviewByAgentApi(Resource): query = query_params_from_request(AgentDriveFileByAgentQuery) resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - _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) @@ -314,7 +266,6 @@ class AgentDriveDownloadByAgentApi(Resource): query = query_params_from_request(AgentDriveFileByAgentQuery) resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) try: - _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) @@ -337,7 +288,7 @@ class AgentDriveListApi(Resource): if not agent_id: return _agent_not_bound() try: - items = _versioned_manifest(tenant_id=app_model.tenant_id, agent_id=agent_id, prefix=query.prefix) + items = AgentDriveService().manifest(tenant_id=app_model.tenant_id, agent_id=agent_id, prefix=query.prefix) except AgentDriveError as exc: return _handle(exc) # the inner manifest exposes file_id for agent-side pulls; the console @@ -361,7 +312,7 @@ class AgentDriveSkillListApi(Resource): if not agent_id: return _agent_not_bound() try: - items = _versioned_skills(tenant_id=app_model.tenant_id, agent_id=agent_id) + items = AgentDriveService().list_skills(tenant_id=app_model.tenant_id, agent_id=agent_id) except AgentDriveError as exc: return _handle(exc) return {"items": items} @@ -389,7 +340,6 @@ class AgentDriveSkillInspectApi(Resource): if not agent_id: return _agent_not_bound() try: - _assert_skill_in_active_soul(tenant_id=app_model.tenant_id, agent_id=agent_id, skill_path=skill_path) return _json_response( AgentDriveService().inspect_skill( tenant_id=app_model.tenant_id, @@ -417,7 +367,6 @@ class AgentDrivePreviewApi(Resource): if not agent_id: return _agent_not_bound() try: - _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) @@ -439,7 +388,6 @@ class AgentDriveDownloadApi(Resource): if not agent_id: return _agent_not_bound() try: - _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) diff --git a/api/controllers/inner_api/plugin/agent_drive.py b/api/controllers/inner_api/plugin/agent_drive.py index 1177b6d8a1e..0cdb9dab35f 100644 --- a/api/controllers/inner_api/plugin/agent_drive.py +++ b/api/controllers/inner_api/plugin/agent_drive.py @@ -17,8 +17,6 @@ from controllers.console.wraps import setup_required 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 services.agent.soul_files_service import AgentSoulFilesService from services.agent_drive_service import ( AgentDriveError, AgentDriveService, @@ -37,28 +35,6 @@ def _error_response(exc: AgentDriveError) -> tuple[dict[str, str], int]: return {"code": exc.code, "message": exc.message}, exc.status_code -def _versioned_manifest( - *, - tenant_id: str, - agent_id: str, - prefix: str = "", - include_download_url: bool = False, -) -> 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 = 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 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") class AgentDriveManifestApi(Resource): @setup_required @@ -72,7 +48,7 @@ class AgentDriveManifestApi(Resource): if not tenant_id: raise AgentDriveError("missing_tenant_id", "tenant_id is required", status_code=400) include_download_url = (request.args.get("include_download_url") or "").lower() in ("1", "true", "yes") - items = _versioned_manifest( + items = AgentDriveService().manifest( tenant_id=tenant_id, agent_id=agent_id, prefix=request.args.get("prefix", ""), @@ -95,7 +71,7 @@ class AgentDriveSkillsApi(Resource): tenant_id = (request.args.get("tenant_id") or "").strip() if not tenant_id: raise AgentDriveError("missing_tenant_id", "tenant_id is required", status_code=400) - items = AgentSoulFilesService.list_skills(session=db.session, tenant_id=tenant_id, agent_id=agent_id) + items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=agent_id) except AgentDriveError as exc: return _error_response(exc) return {"items": items} @@ -121,13 +97,6 @@ class AgentDriveCommitApi(Resource): agent_id=agent_id, items=body.items, ) - AgentSoulFilesService.sync_drive_commit_to_active_soul( - tenant_id=body.tenant_id, - agent_id=agent_id, - account_id=user.id, - committed_items=items, - ) - db.session.commit() except AgentDriveError as exc: return _error_response(exc) return {"items": items} 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 88ceddfdda7..9eab82a8afc 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -71,8 +71,7 @@ from services.agent.prompt_mentions import ( expand_prompt_mentions, parse_prompt_mentions, ) -from services.agent.soul_files_service import AgentSoulFilesService -from services.agent_drive_service import AgentDriveError, AgentDriveService, decode_drive_mention_ref +from services.agent_drive_service import AgentDriveService, decode_drive_mention_ref from .output_failure_orchestrator import retry_idempotency_key from .plugin_tools_builder import WorkflowAgentPluginToolsBuilder, WorkflowAgentPluginToolsBuildError @@ -670,17 +669,13 @@ def build_drive_aware_soul_mention_resolver( tenant_id: str, agent_id: str, ): - """Resolve skill/file mentions against versioned Agent Soul refs and everything else via Agent Soul.""" + """Resolve skill/file mentions against the agent drive and everything else via Agent Soul.""" base_resolver = build_soul_mention_resolver(agent_soul) - skill_names_by_key = { - skill.skill_md_key: skill.name for skill in agent_soul.files.skills if skill.skill_md_key and skill.name - } - file_names_by_key = { - file_ref.drive_key: file_ref.name or file_ref.drive_key.rsplit("/", 1)[-1] - for file_ref in agent_soul.files.files - if file_ref.drive_key - } + drive_service = AgentDriveService() + skill_catalog = drive_service.list_skills(tenant_id=tenant_id, agent_id=agent_id) + skill_names_by_key = {skill["skill_md_key"]: skill["name"] for skill in skill_catalog} + drive_keys = {item["key"] for item in drive_service.manifest(tenant_id=tenant_id, agent_id=agent_id)} def _resolve(mention: object) -> str | None: if not hasattr(mention, "kind") or not hasattr(mention, "ref_id"): @@ -693,7 +688,9 @@ def build_drive_aware_soul_mention_resolver( return skill_names_by_key.get(decoded_key) or label or decoded_key if kind == MentionKind.FILE: decoded_key = decode_drive_mention_ref(ref_id) - return file_names_by_key.get(decoded_key) or label or decoded_key + if decoded_key in drive_keys: + return decoded_key.rsplit("/", 1)[-1] + return label or decoded_key return base_resolver(cast(Any, mention)) return _resolve @@ -705,7 +702,7 @@ def build_drive_layer_config( tenant_id: str, agent_id: str | None, ) -> tuple[DifyDriveLayerConfig | None, list[dict[str, str]]]: - """Derive drive runtime catalog + prompt-mentioned eager-pull keys from Agent Soul refs.""" + """Derive drive runtime catalog + prompt-mentioned eager-pull keys from the drive.""" mentioned_drive_refs = [ decode_drive_mention_ref(mention.ref_id) @@ -724,22 +721,9 @@ def build_drive_layer_config( } ] - skills_catalog = [ - { - "path": skill.path or AgentSoulFilesService.skill_path_from_key(skill.skill_md_key), - "name": skill.name or skill.path or skill.skill_md_key, - "description": skill.description or "", - "skill_md_key": skill.skill_md_key, - "archive_key": skill.full_archive_key, - } - for skill in agent_soul.files.skills - if skill.skill_md_key - ] - 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 = [] + drive_service = AgentDriveService() + skills_catalog = drive_service.list_skills(tenant_id=tenant_id, agent_id=agent_id) + manifest_items = drive_service.manifest(tenant_id=tenant_id, agent_id=agent_id) 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]] = [] @@ -749,7 +733,7 @@ def build_drive_layer_config( if drive_key in skill_keys: mentioned_skill_keys.append(drive_key) continue - if drive_key in soul_file_keys: + if drive_key in manifest_by_key: mentioned_file_keys.append(drive_key) continue warnings.append( @@ -759,15 +743,6 @@ 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/fields/agent_fields.py b/api/fields/agent_fields.py index 6f6ce5c6b4a..783b99850c6 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -17,10 +17,8 @@ from models.agent import ( ) from models.agent_config_entities import ( AgentCliToolConfig, - AgentFileRefConfig, AgentHumanContactConfig, AgentKnowledgeDatasetConfig, - AgentSkillRefConfig, AgentSoulConfig, DeclaredOutputConfig, DeclaredOutputType, @@ -439,8 +437,6 @@ class AgentComposerSoulCandidatesResponse(ResponseModel): cli_tools: list[AgentCliToolConfig] = Field(default_factory=list) knowledge_sets: list[AgentComposerKnowledgeSetCandidateResponse] = Field(default_factory=list) human_contacts: list[AgentHumanContactConfig] = Field(default_factory=list) - skills: list[AgentSkillRefConfig] = Field(default_factory=list) - files: list[AgentFileRefConfig] = Field(default_factory=list) class AgentComposerCandidatesResponse(ResponseModel): diff --git a/api/models/agent_config_entities.py b/api/models/agent_config_entities.py index e31d899fe37..2f81495e9f9 100644 --- a/api/models/agent_config_entities.py +++ b/api/models/agent_config_entities.py @@ -162,18 +162,6 @@ class AgentSkillRefConfig(AgentFlexibleConfig): manifest_files: list[str] | None = None -class AgentSoulFilesConfig(BaseModel): - """Versioned Agent Soul references to drive-backed skills and files. - - File bytes and drive value pointers stay in ``agent_drive_files``. This - section records which drive keys belong to one Agent Soul snapshot so version - restore/copy/runtime use the same skills/files view the user published. - """ - - skills: list[AgentSkillRefConfig] = Field(default_factory=list) - files: list[AgentFileRefConfig] = Field(default_factory=list) - - class AgentPermissionConfig(BaseModel): model_config = ConfigDict(extra="ignore") @@ -691,7 +679,6 @@ class AgentSoulConfig(BaseModel): env: AgentSoulEnvConfig = Field(default_factory=AgentSoulEnvConfig) sandbox: AgentSoulSandboxConfig = Field(default_factory=AgentSoulSandboxConfig) memory: AgentSoulMemoryConfig = Field(default_factory=AgentSoulMemoryConfig) - files: AgentSoulFilesConfig = Field(default_factory=AgentSoulFilesConfig) model: AgentSoulModelConfig | None = None app_features: AgentSoulAppFeaturesConfig = Field(default_factory=AgentSoulAppFeaturesConfig) app_variables: list[AppVariableConfig] = Field(default_factory=list) diff --git a/api/services/agent/composer_candidates.py b/api/services/agent/composer_candidates.py index 4de4766de1b..a650b16e9bc 100644 --- a/api/services/agent/composer_candidates.py +++ b/api/services/agent/composer_candidates.py @@ -172,8 +172,6 @@ def soul_candidates( ) human_contacts = [contact.model_dump(exclude_none=True) for contact in soul.human.contacts] - skills = [skill.model_dump(exclude_none=True) for skill in soul.files.skills] - files = [file_ref.model_dump(exclude_none=True) for file_ref in soul.files.files] dify_tools = workspace_tools_loader() lists = { @@ -181,8 +179,6 @@ def soul_candidates( "cli_tools": cli_tools, "knowledge_sets": knowledge_sets, "human_contacts": human_contacts, - "skills": skills, - "files": files, } capped: dict[str, list[dict[str, Any]]] = {} for key, values in lists.items(): diff --git a/api/services/agent/composer_service.py b/api/services/agent/composer_service.py index be418b60507..a6fbfc73ef5 100644 --- a/api/services/agent/composer_service.py +++ b/api/services/agent/composer_service.py @@ -44,7 +44,6 @@ from services.agent.knowledge_datasets import ( list_missing_tenant_knowledge_dataset_ids, ) from services.agent.roster_service import AgentRosterService -from services.agent.soul_files_service import AgentSoulFilesService from services.app_service import AppService, CreateAppParams from services.entities.agent_entities import ( AgentSoulConfig, @@ -323,11 +322,6 @@ class AgentComposerService: except IntegrityError as exc: db.session.rollback() raise AgentNameConflictError() from exc - payload.agent_soul = cls._preserve_agent_draft_soul_files( - tenant_id=tenant_id, - agent_id=agent.id, - agent_soul=payload.agent_soul, - ) cls._save_agent_draft( tenant_id=tenant_id, agent=agent, @@ -453,13 +447,6 @@ class AgentComposerService: ComposerConfigValidator.validate_draft_save_payload(payload) cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul) agent = cls._require_agent(tenant_id=tenant_id, agent_id=agent_id) - payload.agent_soul = cls._preserve_agent_draft_soul_files( - tenant_id=tenant_id, - agent_id=agent.id, - agent_soul=payload.agent_soul, - draft_type=AgentConfigDraftType.DEBUG_BUILD, - account_id=account_id, - ) build_draft = cls._save_agent_draft( tenant_id=tenant_id, agent=agent, @@ -531,7 +518,7 @@ class AgentComposerService: cls._drive_mention_findings( tenant_id=tenant_id, agent_id=agent_id, - agent_soul=payload.agent_soul, + prompt=payload.agent_soul.prompt.system_prompt, ) ) return findings @@ -585,16 +572,14 @@ class AgentComposerService: *, tenant_id: str, agent_id: str, - agent_soul: AgentSoulConfig, + prompt: str, ) -> list[dict[str, str | None]]: """Soft warnings for missing drive-backed prompt mentions.""" from services.agent.prompt_mentions import MentionKind, parse_prompt_mentions from services.agent_drive_service import decode_drive_mention_ref - soul_skill_keys = {skill.skill_md_key for skill in agent_soul.files.skills if skill.skill_md_key} - soul_file_keys = {file_ref.drive_key for file_ref in agent_soul.files.files if file_ref.drive_key} wanted_keys: dict[str, tuple[str, str]] = {} - for mention in parse_prompt_mentions(agent_soul.prompt.system_prompt): + for mention in parse_prompt_mentions(prompt): if mention.kind not in {MentionKind.SKILL, MentionKind.FILE}: continue decoded_key = decode_drive_mention_ref(mention.ref_id) @@ -615,28 +600,6 @@ class AgentComposerService: ) findings: list[dict[str, str | None]] = [] for key, (kind, display) in wanted_keys.items(): - if kind == MentionKind.SKILL.value and key not in soul_skill_keys: - findings.append( - { - "code": "mention_target_missing", - "surface": "agent_soul", - "kind": kind, - "id": key, - "message": f"{kind} '{display}' is not recorded in this Agent Soul version.", - } - ) - continue - if kind == MentionKind.FILE.value and key not in soul_file_keys: - findings.append( - { - "code": "mention_target_missing", - "surface": "agent_soul", - "kind": kind, - "id": key, - "message": f"{kind} '{display}' is not recorded in this Agent Soul version.", - } - ) - continue if key in existing_keys: continue findings.append( @@ -916,11 +879,6 @@ class AgentComposerService: ) binding.node_job_config = node_job if payload.agent_soul is not None and binding.binding_type == WorkflowAgentBindingType.INLINE_AGENT: - payload.agent_soul = cls._preserve_active_soul_files( - tenant_id=tenant_id, - agent_id=binding.agent_id, - agent_soul=payload.agent_soul, - ) current_snapshot = cls._require_version( tenant_id=tenant_id, agent_id=binding.agent_id, @@ -1021,11 +979,6 @@ class AgentComposerService: binding = cls._require_binding(binding) if payload.agent_soul is None: raise ValueError("agent_soul is required") - payload.agent_soul = cls._preserve_active_soul_files( - tenant_id=tenant_id, - agent_id=binding.agent_id, - agent_soul=payload.agent_soul, - ) current_snapshot = cls._require_version( tenant_id=tenant_id, agent_id=binding.agent_id, @@ -1060,11 +1013,6 @@ class AgentComposerService: binding = cls._require_binding(binding) if not binding.agent_id or payload.agent_soul is None: raise ValueError("agent_id and agent_soul are required") - payload.agent_soul = cls._preserve_active_soul_files( - tenant_id=tenant_id, - agent_id=binding.agent_id, - agent_soul=payload.agent_soul, - ) version = cls._create_config_version( tenant_id=tenant_id, agent_id=binding.agent_id, @@ -1097,12 +1045,6 @@ class AgentComposerService: ) -> WorkflowAgentNodeBinding: if payload.agent_soul is None: raise ValueError("agent_soul is required") - if binding and binding.agent_id: - payload.agent_soul = cls._preserve_active_soul_files( - tenant_id=tenant_id, - agent_id=binding.agent_id, - agent_soul=payload.agent_soul, - ) agent_name = payload.new_agent_name or "Untitled Agent" agent = cls._create_roster_agent_for_composer( tenant_id=tenant_id, @@ -1118,15 +1060,6 @@ class AgentComposerService: version_note=payload.version_note, ) node_job = payload.node_job or WorkflowNodeJobConfig() - if binding and binding.agent_id: - cls._copy_agent_drive_rows( - tenant_id=tenant_id, - source_agent_id=binding.agent_id, - target_agent_id=agent.id, - account_id=account_id, - agent_soul=payload.agent_soul, - node_job=node_job, - ) if not binding: binding = WorkflowAgentNodeBinding( tenant_id=tenant_id, @@ -1162,9 +1095,6 @@ class AgentComposerService: version_id=binding.current_snapshot_id, ) agent_soul = payload.agent_soul or AgentSoulConfig.model_validate(source_version.config_snapshot_dict) - source_soul = AgentSoulConfig.model_validate(source_version.config_snapshot_dict) - agent_soul = agent_soul.model_copy(deep=True) - agent_soul.files = source_soul.files agent_name = payload.new_agent_name or source_agent.name roster_agent = cls._create_roster_agent_for_composer( tenant_id=tenant_id, @@ -1306,63 +1236,26 @@ class AgentComposerService: ) ) - @classmethod - def _preserve_active_soul_files( - cls, - *, - tenant_id: str, - agent_id: str | None, - agent_soul: AgentSoulConfig, - ) -> AgentSoulConfig: - """Keep drive refs owned by drive APIs when saving non-file composer changes.""" - - if not agent_id: - return agent_soul - agent = cls._get_agent_if_present(tenant_id=tenant_id, agent_id=agent_id) - if agent is None or not agent.active_config_snapshot_id: - return agent_soul - version = cls._get_version_if_present( - tenant_id=tenant_id, - agent_id=agent.id, - version_id=agent.active_config_snapshot_id, - ) - if version is None: - return agent_soul - existing_soul = AgentSoulConfig.model_validate(version.config_snapshot_dict) - preserved = agent_soul.model_copy(deep=True) - preserved.files = existing_soul.files - return preserved - - @classmethod - def _preserve_agent_draft_soul_files( - cls, - *, - tenant_id: str, - agent_id: str | None, - agent_soul: AgentSoulConfig, - draft_type: AgentConfigDraftType = AgentConfigDraftType.DRAFT, - account_id: str | None = None, - ) -> AgentSoulConfig: - if not agent_id: - return agent_soul - draft = cls._get_agent_draft( - tenant_id=tenant_id, - agent_id=agent_id, - draft_type=draft_type, - account_id=account_id, - ) - if draft is not None: - existing_soul = AgentSoulConfig.model_validate(draft.config_snapshot_dict) - preserved = agent_soul.model_copy(deep=True) - preserved.files = existing_soul.files - return preserved - return cls._preserve_active_soul_files(tenant_id=tenant_id, agent_id=agent_id, agent_soul=agent_soul) - @staticmethod def _drive_copy_scopes_from_agent_configs( *, agent_soul: AgentSoulConfig, node_job: WorkflowNodeJobConfig | None = None ) -> tuple[set[str], set[str]]: - exact_keys, prefixes = AgentSoulFilesService.drive_copy_scopes(agent_soul=agent_soul) + from services.agent.prompt_mentions import MentionKind, parse_prompt_mentions + from services.agent_drive_service import decode_drive_mention_ref + + exact_keys: set[str] = set() + prefixes: set[str] = set() + + for mention in parse_prompt_mentions(agent_soul.prompt.system_prompt): + if mention.kind not in {MentionKind.SKILL, MentionKind.FILE}: + continue + drive_key = decode_drive_mention_ref(mention.ref_id) + if not drive_key: + continue + if mention.kind == MentionKind.SKILL and "/" in drive_key: + prefixes.add(f"{drive_key.rsplit('/', 1)[0]}/") + else: + exact_keys.add(drive_key) if node_job is not None: for file_ref in node_job.metadata.file_refs or []: diff --git a/api/services/agent/skill_standardize_service.py b/api/services/agent/skill_standardize_service.py index 579c9591682..08b6b5bdf9e 100644 --- a/api/services/agent/skill_standardize_service.py +++ b/api/services/agent/skill_standardize_service.py @@ -45,7 +45,6 @@ class SkillStandardizeService: self._package = package_service or SkillPackageService() self._drive = drive_service or AgentDriveService() self._tool_files = tool_file_manager or ToolFileManager() - self.last_committed_items: list[dict[str, Any]] = [] def standardize( self, @@ -81,7 +80,31 @@ class SkillStandardizeService: skill_md_key = f"{slug}/{_SKILL_MD_NAME}" archive_key = f"{slug}/{_FULL_ARCHIVE_NAME}" - committed_items = self._drive.commit( + member_items: list[DriveCommitItem] = [] + for member_path in sorted(set(manifest.files)): + member_key = f"{slug}/{member_path}" + if member_key in {skill_md_key, archive_key}: + continue + + member_bytes = self._package.read_member_bytes(content=content, member_path=member_path) + mimetype = mimetypes.guess_type(member_path)[0] or "application/octet-stream" + member_tool_file = self._tool_files.create_file_by_raw( + user_id=user_id, + tenant_id=tenant_id, + conversation_id=None, + file_binary=member_bytes, + mimetype=mimetype, + filename=posixpath.basename(member_path), + ) + member_items.append( + DriveCommitItem( + key=member_key, + file_ref=DriveFileRef(kind="tool_file", id=member_tool_file.id), + value_owned_by_drive=True, + ) + ) + + self._drive.commit( tenant_id=tenant_id, user_id=user_id, agent_id=agent_id, @@ -102,9 +125,9 @@ class SkillStandardizeService: file_ref=DriveFileRef(kind="tool_file", id=archive_tool_file.id), value_owned_by_drive=True, ), + *member_items, ], ) - self.last_committed_items = committed_items drive_skill = next( skill diff --git a/api/services/agent/soul_files_service.py b/api/services/agent/soul_files_service.py deleted file mode 100644 index 2de16337352..00000000000 --- a/api/services/agent/soul_files_service.py +++ /dev/null @@ -1,390 +0,0 @@ -from __future__ import annotations - -import json -from typing import Any - -from sqlalchemy import func, select -from sqlalchemy.orm import Session - -from extensions.ext_database import db -from models.agent import ( - Agent, - AgentConfigRevision, - AgentConfigRevisionOperation, - AgentConfigSnapshot, - AgentDriveFile, -) -from models.agent_config_entities import AgentFileRefConfig, AgentSkillRefConfig, AgentSoulConfig -from services.agent.agent_soul_state import agent_soul_has_model -from services.agent_drive_service import AgentDriveError, DriveSkillMetadata, normalize_drive_key - -_SKILL_MD_SUFFIX = "/SKILL.md" -_SKILL_ARCHIVE_NAME = ".DIFY-SKILL-FULL.zip" -_FILES_PREFIX = "files/" - - -class AgentSoulFilesService: - """Versioned Agent Soul view of drive-backed skills and files. - - ``agent_drive_files`` remains the storage/index for bytes and drive values. - ``AgentSoulConfig.files`` records the versioned pointers that a specific - Agent Soul snapshot owns, so restore/publish/runtime do not accidentally see - later drive mutations. - """ - - @classmethod - def sync_drive_commit_to_active_soul( - cls, - *, - tenant_id: str, - agent_id: str, - account_id: str, - committed_items: list[dict[str, Any]], - ) -> AgentConfigSnapshot | None: - if not committed_items: - return None - - agent = db.session.scalar(select(Agent).where(Agent.tenant_id == tenant_id, Agent.id == agent_id)) - if agent is None or not agent.active_config_snapshot_id: - return None - current_snapshot = db.session.scalar( - select(AgentConfigSnapshot).where( - AgentConfigSnapshot.tenant_id == tenant_id, - AgentConfigSnapshot.agent_id == agent_id, - AgentConfigSnapshot.id == agent.active_config_snapshot_id, - ) - ) - if current_snapshot is None: - return None - - agent_soul = AgentSoulConfig.model_validate(current_snapshot.config_snapshot_dict).model_copy(deep=True) - before = agent_soul.files.model_dump(mode="json") - for item in committed_items: - cls._apply_commit_item(agent_soul=agent_soul, item=item) - if agent_soul.files.model_dump(mode="json") == before: - return None - - version = cls._create_config_version( - tenant_id=tenant_id, - agent_id=agent_id, - account_id=account_id, - agent_soul=agent_soul, - previous_snapshot_id=current_snapshot.id, - ) - agent.active_config_snapshot_id = version.id - agent.active_config_has_model = agent_soul_has_model(agent_soul) - agent.updated_by = account_id - db.session.flush() - return version - - @classmethod - def list_files( - 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) - file_keys = [file.drive_key for file in agent_soul.files.files if file.drive_key] - if prefix: - normalized_prefix = normalize_drive_key(prefix) - file_keys = [key for key in file_keys if key.startswith(normalized_prefix)] - if not file_keys: - return [] - - rows = cls._drive_rows_by_key(session=session, tenant_id=tenant_id, agent_id=agent_id, keys=file_keys) - items: list[dict[str, Any]] = [] - for file_ref in agent_soul.files.files: - key = file_ref.drive_key - if not key or key not in file_keys: - 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}) - items.append(item) - return items - - @classmethod - def list_skills( - cls, - *, - session: Session, - tenant_id: str, - agent_id: str, - ) -> list[dict[str, Any]]: - agent_soul = cls.active_agent_soul(session=session, tenant_id=tenant_id, agent_id=agent_id) - skill_keys = [skill.skill_md_key for skill in agent_soul.files.skills if skill.skill_md_key] - archive_keys = [skill.full_archive_key for skill in agent_soul.files.skills if skill.full_archive_key] - rows = cls._drive_rows_by_key( - session=session, tenant_id=tenant_id, agent_id=agent_id, keys=[*skill_keys, *archive_keys] - ) - items: list[dict[str, Any]] = [] - for skill in agent_soul.files.skills: - if not skill.skill_md_key: - continue - row = rows.get(skill.skill_md_key) - archive_key = skill.full_archive_key if skill.full_archive_key in rows else None - 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, - "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 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, - } - ) - return items - - @classmethod - def allowed_drive_keys(cls, agent_soul: AgentSoulConfig) -> set[str]: - keys: set[str] = set() - for file_ref in agent_soul.files.files: - if file_ref.drive_key: - keys.add(file_ref.drive_key) - for skill in agent_soul.files.skills: - if skill.skill_md_key: - keys.add(skill.skill_md_key) - if skill.full_archive_key: - keys.add(skill.full_archive_key) - return keys - - @classmethod - def allowed_skill_prefixes(cls, agent_soul: AgentSoulConfig) -> set[str]: - prefixes: set[str] = set() - for skill in agent_soul.files.skills: - path = skill.path or (cls.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else None) - if path: - prefixes.add(f"{path}/") - return prefixes - - @classmethod - def key_allowed_by_soul(cls, *, agent_soul: AgentSoulConfig, key: str) -> bool: - normalized_key = normalize_drive_key(key) - if normalized_key in cls.allowed_drive_keys(agent_soul): - return True - return any(normalized_key.startswith(prefix) for prefix in cls.allowed_skill_prefixes(agent_soul)) - - @classmethod - def drive_copy_scopes(cls, *, agent_soul: AgentSoulConfig) -> tuple[set[str], set[str]]: - exact_keys = cls.allowed_drive_keys(agent_soul) - prefixes = cls.allowed_skill_prefixes(agent_soul) - return exact_keys, prefixes - - @staticmethod - def active_agent_soul(*, session: Session, tenant_id: str, agent_id: str) -> AgentSoulConfig: - snapshot = session.scalar( - select(AgentConfigSnapshot) - .join(Agent, Agent.active_config_snapshot_id == AgentConfigSnapshot.id) - .where( - Agent.tenant_id == tenant_id, - Agent.id == agent_id, - AgentConfigSnapshot.tenant_id == tenant_id, - AgentConfigSnapshot.agent_id == agent_id, - ) - ) - if snapshot is None: - raise AgentDriveError( - "agent_snapshot_not_found", - "agent has no active Agent Soul snapshot", - status_code=404, - ) - return AgentSoulConfig.model_validate(snapshot.config_snapshot_dict) - - @staticmethod - def skill_path_from_key(key: str) -> str: - if not key.endswith(_SKILL_MD_SUFFIX): - raise AgentDriveError( - "invalid_skill_key", - "skill rows must use the canonical '/SKILL.md' key", - status_code=500, - ) - return key[: -len(_SKILL_MD_SUFFIX)] - - @staticmethod - def skill_archive_key(skill_md_key: str) -> str: - return f"{AgentSoulFilesService.skill_path_from_key(skill_md_key)}/{_SKILL_ARCHIVE_NAME}" - - @classmethod - def _apply_commit_item(cls, *, agent_soul: AgentSoulConfig, item: dict[str, Any]) -> None: - key = normalize_drive_key(str(item.get("key") or "")) - if item.get("removed"): - cls._remove_ref(agent_soul=agent_soul, key=key) - return - - if item.get("is_skill"): - cls._upsert_skill_ref(agent_soul=agent_soul, key=key, item=item) - return - if key.startswith(_FILES_PREFIX): - cls._upsert_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: - metadata = cls._parse_skill_metadata(item.get("skill_metadata")) - path = cls.skill_path_from_key(key) - ref = AgentSkillRefConfig( - id=path, - name=metadata.name, - description=metadata.description, - file_id=str(item.get("file_id") or ""), - path=path, - skill_md_key=key, - skill_md_file_id=str(item.get("file_id") or ""), - full_archive_key=cls.skill_archive_key(key), - manifest_files=metadata.manifest_files, - ) - skills = [ - existing for existing in agent_soul.files.skills if existing.skill_md_key != key and existing.path != path - ] - skills.append(ref) - skills.sort(key=lambda value: value.path or value.skill_md_key or "") - agent_soul.files.skills = skills - - @staticmethod - def _upsert_file_ref(*, agent_soul: AgentSoulConfig, key: str, item: dict[str, Any]) -> None: - name = key.removeprefix(_FILES_PREFIX) or key.rsplit("/", 1)[-1] - file_id = str(item.get("file_id") or "") - ref = AgentFileRefConfig( - id=key, - file_id=file_id, - upload_file_id=file_id if item.get("file_kind") == "upload_file" else None, - name=name, - type=str(item.get("mime_type") or ""), - transfer_method=str(item.get("file_kind") or ""), - drive_key=key, - ) - 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 _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] - if key.endswith(_SKILL_MD_SUFFIX): - path = cls.skill_path_from_key(key) - agent_soul.files.skills = [ - skill for skill in agent_soul.files.skills if skill.skill_md_key != key and skill.path != path - ] - return - if key.endswith(f"/{_SKILL_ARCHIVE_NAME}"): - agent_soul.files.skills = [ - skill.model_copy(update={"full_archive_key": None}) - if skill.full_archive_key == key - else skill - for skill in agent_soul.files.skills - ] - - @staticmethod - def _parse_skill_metadata(raw_metadata: Any) -> DriveSkillMetadata: - if isinstance(raw_metadata, DriveSkillMetadata): - return raw_metadata - if isinstance(raw_metadata, str): - return DriveSkillMetadata.model_validate(json.loads(raw_metadata)) - return DriveSkillMetadata.model_validate(raw_metadata or {}) - - @staticmethod - def _drive_rows_by_key( - *, - session: Session, - tenant_id: str, - agent_id: str, - keys: list[str], - ) -> dict[str, AgentDriveFile]: - if not keys: - return {} - return { - row.key: row - for row in session.scalars( - select(AgentDriveFile).where( - AgentDriveFile.tenant_id == tenant_id, - AgentDriveFile.agent_id == agent_id, - AgentDriveFile.key.in_(sorted(set(keys))), - ) - ) - } - - @staticmethod - def _row_item(row: AgentDriveFile | None) -> dict[str, Any]: - if row is None: - return {} - return { - "key": row.key, - "size": row.size, - "hash": row.hash, - "mime_type": row.mime_type, - "file_kind": row.file_kind.value, - "is_skill": row.is_skill, - "skill_metadata": row.skill_metadata, - "created_at": int(row.created_at.timestamp()) if row.created_at else None, - } - - @staticmethod - def _file_item_from_ref(file_ref: AgentFileRefConfig) -> dict[str, Any]: - key = file_ref.drive_key or file_ref.name - return { - "key": key, - "name": file_ref.name, - "mime_type": file_ref.type, - "file_kind": file_ref.transfer_method, - "is_skill": False, - } - - @classmethod - def _create_config_version( - cls, - *, - tenant_id: str, - agent_id: str, - account_id: str, - agent_soul: AgentSoulConfig, - previous_snapshot_id: str, - ) -> AgentConfigSnapshot: - next_version = ( - db.session.scalar( - select(func.max(AgentConfigSnapshot.version)).where( - AgentConfigSnapshot.tenant_id == tenant_id, - AgentConfigSnapshot.agent_id == agent_id, - ) - ) - or 0 - ) + 1 - version = AgentConfigSnapshot( - tenant_id=tenant_id, - agent_id=agent_id, - version=next_version, - config_snapshot=agent_soul, - created_by=account_id, - ) - db.session.add(version) - db.session.flush() - revision = AgentConfigRevision( - tenant_id=tenant_id, - agent_id=agent_id, - previous_snapshot_id=previous_snapshot_id, - current_snapshot_id=version.id, - revision=cls._next_revision(tenant_id=tenant_id, agent_id=agent_id), - operation=AgentConfigRevisionOperation.SAVE_CURRENT_VERSION, - created_by=account_id, - ) - db.session.add(revision) - db.session.flush() - return version - - @staticmethod - def _next_revision(*, tenant_id: str, agent_id: str) -> int: - return ( - db.session.scalar( - select(func.max(AgentConfigRevision.revision)).where( - AgentConfigRevision.tenant_id == tenant_id, - AgentConfigRevision.agent_id == agent_id, - ) - ) - or 0 - ) + 1 diff --git a/api/services/agent/workflow_publish_service.py b/api/services/agent/workflow_publish_service.py index 9d60b9dd63d..8090a643bc7 100644 --- a/api/services/agent/workflow_publish_service.py +++ b/api/services/agent/workflow_publish_service.py @@ -22,7 +22,6 @@ from models.agent import ( from models.agent_config_entities import AgentSoulConfig, DeclaredOutputConfig, WorkflowNodeJobConfig from models.workflow import Workflow from services.agent.composer_validator import ComposerConfigValidator -from services.agent.soul_files_service import AgentSoulFilesService from services.entities.agent_entities import ( ComposerSavePayload, ComposerSaveStrategy, @@ -183,8 +182,6 @@ class WorkflowAgentPublishService: from services.agent_drive_service import decode_drive_mention_ref 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 = {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 @@ -193,37 +190,24 @@ class WorkflowAgentPublishService: continue code = "skill_ref_dangling" if mention.kind == MentionKind.SKILL else "file_ref_dangling" wanted_keys[drive_key] = (code, mention.label or drive_key) - if not binding.agent_id: + if not wanted_keys or not binding.agent_id: return - declared_keys, _ = AgentSoulFilesService.drive_copy_scopes(agent_soul=agent_soul) - check_keys = sorted(set(wanted_keys) | declared_keys) - if not check_keys: - return 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), + AgentDriveFile.key.in_(sorted(wanted_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: - messages.append(f"{code}: skill '{display}' is not recorded in this Agent Soul version.") - continue - if code == "file_ref_dangling" and key not in soul_file_keys: - messages.append(f"{code}: file '{display}' is not recorded in this Agent Soul version.") - continue if key in existing_keys: continue kind = "skill" if code == "skill_ref_dangling" else "file" messages.append(f"{code}: {kind} '{display}' has no drive entry for key '{key}'.") - for key in declared_keys: - if key not in existing_keys: - messages.append(f"drive_ref_dangling: Agent Soul drive ref '{key}' has no backing drive entry.") if messages: raise WorkflowAgentNodeValidationError( f"Workflow Agent node {binding.node_id} has invalid Agent Soul drive refs: {'; '.join(messages)}" 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 baf316080fd..dc5a3e9720b 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 @@ -941,19 +941,6 @@ def _soul_with_drive_skill() -> AgentSoulConfig: "and [§file:files%2Fsample.pdf:sample.pdf§]." ) }, - files={ - "skills": [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "description": "Parses RFPs.", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - } - ], - "files": [{"id": "files/sample.pdf", "name": "sample.pdf", "drive_key": "files/sample.pdf"}], - }, model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"), ) @@ -1135,7 +1122,6 @@ def test_workflow_runtime_missing_drive_mentions_fall_back_to_label_then_decoded "and [§file:files%2Fno-label.txt§]." ) }, - files={"files": [{"id": "files/no-label.txt", "name": "no-label.txt", "drive_key": "files/no-label.txt"}]}, model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"), ) diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index 337d742fe9f..ebf9eb35025 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -1240,16 +1240,6 @@ def test_copy_agent_drive_rows_copies_skill_prefix_and_files(monkeypatch: pytest "prompt": { "system_prompt": "[§skill:tender-analyzer/SKILL.md:Tender Analyzer§]", }, - "files": { - "skills": [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - } - ], - }, } ) node_job = WorkflowNodeJobConfig.model_validate( @@ -1329,17 +1319,6 @@ def test_drive_copy_scopes_include_declared_output_benchmark_files(): "[§skill:tender-analyzer/SKILL.md:Tender Analyzer§]" ) }, - "files": { - "skills": [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - } - ], - "files": [{"id": "files/source.pdf", "name": "source.pdf", "drive_key": "files/source.pdf"}], - }, } ) node_job = WorkflowNodeJobConfig.model_validate( @@ -3910,18 +3889,6 @@ def _drive_soul(**overrides): "Use [§skill:tender-analyzer%2FSKILL.md:Tender Analyzer§] and [§file:files%2Fsample.pdf:sample.pdf§]." ) }, - "files": { - "skills": [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - } - ], - "files": [{"id": "files/sample.pdf", "name": "sample.pdf", "drive_key": "files/sample.pdf"}], - }, } base.update(overrides) return AgentSoulConfig.model_validate(base) @@ -3946,7 +3913,7 @@ def test_drive_mention_findings_reports_missing_keys(monkeypatch: pytest.MonkeyP findings = AgentComposerService._drive_mention_findings( tenant_id="tenant-1", agent_id="agent-1", - agent_soul=_drive_soul(), + prompt=_drive_soul().prompt.system_prompt, ) assert [(f["code"], f["id"]) for f in findings] == [("mention_target_missing", "files/sample.pdf")] @@ -3961,7 +3928,7 @@ def test_drive_mention_findings_clean_when_all_keys_exist(monkeypatch: pytest.Mo AgentComposerService._drive_mention_findings( tenant_id="tenant-1", agent_id="agent-1", - agent_soul=_drive_soul(), + prompt=_drive_soul().prompt.system_prompt, ) == [] ) @@ -3973,7 +3940,7 @@ def test_drive_mention_findings_skips_prompt_without_drive_mentions(monkeypatch: findings = AgentComposerService._drive_mention_findings( tenant_id="tenant-1", agent_id="agent-1", - agent_soul=soul, + prompt=soul.prompt.system_prompt, ) assert findings == [] 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 deleted file mode 100644 index 8e181189390..00000000000 --- a/api/tests/unit_tests/services/agent/test_agent_soul_files_service.py +++ /dev/null @@ -1,118 +0,0 @@ -import json - -from models.agent_config_entities import AgentSoulConfig -from services.agent.soul_files_service import AgentSoulFilesService - - -def test_apply_drive_commit_records_skill_and_file_refs_in_agent_soul(): - soul = AgentSoulConfig() - - AgentSoulFilesService._apply_commit_item( - agent_soul=soul, - item={ - "key": "tender-analyzer/SKILL.md", - "file_kind": "tool_file", - "file_id": "skill-md-file", - "is_skill": True, - "skill_metadata": json.dumps( - { - "name": "Tender Analyzer", - "description": "Parses tenders.", - "manifest_files": ["SKILL.md", "src/main.py"], - } - ), - }, - ) - AgentSoulFilesService._apply_commit_item( - agent_soul=soul, - item={ - "key": "files/sample.pdf", - "file_kind": "upload_file", - "file_id": "upload-file", - "mime_type": "application/pdf", - "is_skill": False, - }, - ) - - assert [skill.model_dump(mode="json", exclude_none=True) for skill in soul.files.skills] == [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "description": "Parses tenders.", - "file_id": "skill-md-file", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - "skill_md_file_id": "skill-md-file", - "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - "manifest_files": ["SKILL.md", "src/main.py"], - } - ] - assert [file_ref.model_dump(mode="json", exclude_none=True) for file_ref in soul.files.files] == [ - { - "id": "files/sample.pdf", - "file_id": "upload-file", - "upload_file_id": "upload-file", - "name": "sample.pdf", - "type": "application/pdf", - "transfer_method": "upload_file", - "drive_key": "files/sample.pdf", - } - ] - - -def test_apply_drive_commit_removes_refs_without_touching_unrelated_entries(): - soul = AgentSoulConfig.model_validate( - { - "files": { - "skills": [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - } - ], - "files": [ - {"id": "files/sample.pdf", "name": "sample.pdf", "drive_key": "files/sample.pdf"}, - {"id": "files/keep.pdf", "name": "keep.pdf", "drive_key": "files/keep.pdf"}, - ], - } - } - ) - - AgentSoulFilesService._apply_commit_item(agent_soul=soul, item={"key": "files/sample.pdf", "removed": True}) - AgentSoulFilesService._apply_commit_item(agent_soul=soul, item={"key": "tender-analyzer/SKILL.md", "removed": True}) - - assert [file_ref.drive_key for file_ref in soul.files.files] == ["files/keep.pdf"] - assert soul.files.skills == [] - - -def test_drive_copy_and_access_scopes_come_from_agent_soul_files(): - soul = AgentSoulConfig.model_validate( - { - "files": { - "skills": [ - { - "id": "tender-analyzer", - "name": "Tender Analyzer", - "path": "tender-analyzer", - "skill_md_key": "tender-analyzer/SKILL.md", - "full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip", - } - ], - "files": [{"id": "files/sample.pdf", "name": "sample.pdf", "drive_key": "files/sample.pdf"}], - } - } - ) - - exact_keys, prefixes = AgentSoulFilesService.drive_copy_scopes(agent_soul=soul) - - assert exact_keys == { - "tender-analyzer/SKILL.md", - "tender-analyzer/.DIFY-SKILL-FULL.zip", - "files/sample.pdf", - } - 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 diff --git a/api/tests/unit_tests/services/agent/test_skill_standardize_service.py b/api/tests/unit_tests/services/agent/test_skill_standardize_service.py index 6a8ec4391fd..57d419efc96 100644 --- a/api/tests/unit_tests/services/agent/test_skill_standardize_service.py +++ b/api/tests/unit_tests/services/agent/test_skill_standardize_service.py @@ -64,7 +64,6 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest( user_id="user-1", agent_id="agent-1", ) - assert service.last_committed_items == [] # ToolFiles: SKILL.md and the full archive. Archive members stay lazy. assert tool_files.create_file_by_raw.call_count == 2 @@ -98,4 +97,3 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest( assert skill["archive_key"] == "pdf-toolkit/.DIFY-SKILL-FULL.zip" assert skill["skill_md_key"] == "pdf-toolkit/SKILL.md" assert result["manifest"]["files"] == ["SKILL.md", "scripts/run.py"] - assert "_committed_items" not in result