mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
chore(agent-v2): sync changes (#38442)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: zyssyz123 <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 林玮 (Jade Lin) <linw1995@icloud.com> Co-authored-by: 盐粒 Yanli <mail@yanli.one>
This commit is contained in:
co-authored by
Joel
zyssyz123
autofix-ci[bot]
林玮
盐粒 Yanli
parent
bdb3469ca0
commit
d0ea5a5e0d
@@ -33,6 +33,7 @@ from models.workflow import Workflow
|
||||
from services.agent.agent_soul_state import agent_soul_has_model
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import (
|
||||
AgentModelNotConfiguredError,
|
||||
AgentNameConflictError,
|
||||
AgentNotFoundError,
|
||||
AgentVersionConflictError,
|
||||
@@ -168,7 +169,8 @@ class AgentComposerService:
|
||||
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
_validate_composer_payload_for_strategy(payload)
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
if payload.save_strategy in _PUBLISH_SAVE_STRATEGIES:
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
workflow = cls._get_draft_workflow(tenant_id=tenant_id, app_id=app_id)
|
||||
binding = cls._get_workflow_binding(tenant_id=tenant_id, workflow_id=workflow.id, node_id=node_id)
|
||||
|
||||
@@ -357,7 +359,6 @@ class AgentComposerService:
|
||||
raise ValueError("agent_soul is required")
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
_validate_composer_payload_for_strategy(payload)
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul)
|
||||
|
||||
agent = cls._get_agent_app_agent(tenant_id=tenant_id, app_id=app_id)
|
||||
if not agent:
|
||||
@@ -401,7 +402,6 @@ class AgentComposerService:
|
||||
raise ValueError("agent_soul is required")
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
_validate_composer_payload_for_strategy(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)
|
||||
return cls._save_agent_composer_for_agent(
|
||||
tenant_id=tenant_id,
|
||||
@@ -511,6 +511,8 @@ class AgentComposerService:
|
||||
version_note=version_note,
|
||||
)
|
||||
)
|
||||
if not agent_soul_has_model(agent_soul):
|
||||
raise AgentModelNotConfiguredError()
|
||||
cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=agent_soul)
|
||||
version = cls._create_config_version(
|
||||
tenant_id=tenant_id,
|
||||
@@ -591,7 +593,6 @@ class AgentComposerService:
|
||||
raise ValueError("agent_soul is required")
|
||||
_backfill_cli_tool_ids(payload.agent_soul)
|
||||
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)
|
||||
build_draft = cls._save_agent_draft(
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from models.agent_config_entities import AgentKnowledgeQueryMode
|
||||
from services.agent.errors import AgentSoulLockedError, InvalidComposerConfigError, PlaintextSecretNotAllowedError
|
||||
from services.agent.prompt_mentions import (
|
||||
MAX_MENTIONS_PER_PROMPT,
|
||||
@@ -228,9 +229,40 @@ class ComposerConfigValidator:
|
||||
@classmethod
|
||||
def validate_agent_soul(cls, agent_soul: AgentSoulConfig) -> None:
|
||||
dumped = agent_soul.model_dump(mode="json")
|
||||
cls._validate_knowledge_runtime_config(agent_soul)
|
||||
cls._reject_plaintext_secrets(dumped, path="agent_soul")
|
||||
cls._validate_shell_config(dumped)
|
||||
|
||||
@classmethod
|
||||
def _validate_knowledge_runtime_config(cls, agent_soul: AgentSoulConfig) -> None:
|
||||
"""Validate knowledge settings that are required only for publish/run.
|
||||
|
||||
Draft composer saves must be able to persist partially configured
|
||||
knowledge sets while a user is still editing the panel. These checks
|
||||
stay in the publish validator so invalid runtime configs are still
|
||||
blocked before a version can be published or executed.
|
||||
"""
|
||||
for knowledge_set in agent_soul.knowledge.sets:
|
||||
if (
|
||||
knowledge_set.query.mode == AgentKnowledgeQueryMode.USER_QUERY
|
||||
and not (knowledge_set.query.value or "").strip()
|
||||
):
|
||||
raise InvalidComposerConfigError("knowledge query.value is required for user_query mode")
|
||||
|
||||
retrieval = knowledge_set.retrieval
|
||||
if retrieval.mode == "multiple" and retrieval.top_k is None:
|
||||
raise InvalidComposerConfigError("knowledge retrieval.top_k is required for multiple mode")
|
||||
if retrieval.mode == "single" and retrieval.model is None:
|
||||
raise InvalidComposerConfigError("knowledge retrieval.model is required for single mode")
|
||||
|
||||
metadata_filtering = knowledge_set.metadata_filtering
|
||||
if metadata_filtering.mode == "automatic" and metadata_filtering.metadata_model_config is None:
|
||||
raise InvalidComposerConfigError("metadata_filtering.model_config is required for automatic mode")
|
||||
if metadata_filtering.mode == "manual" and (
|
||||
metadata_filtering.conditions is None or not metadata_filtering.conditions.conditions
|
||||
):
|
||||
raise InvalidComposerConfigError("metadata_filtering.conditions is required for manual mode")
|
||||
|
||||
@classmethod
|
||||
def validate_node_job(cls, node_job: WorkflowNodeJobConfig) -> None:
|
||||
cls._reject_plaintext_secrets(node_job.model_dump(mode="json"), path="node_job")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
||||
|
||||
from libs.exception import BaseHTTPException
|
||||
|
||||
|
||||
class AgentNotFoundError(NotFound):
|
||||
description = "Agent not found."
|
||||
@@ -21,6 +23,12 @@ class AgentVersionConflictError(Conflict):
|
||||
description = "Agent config version changed. Please reload and try again."
|
||||
|
||||
|
||||
class AgentModelNotConfiguredError(BaseHTTPException):
|
||||
error_code = "agent_model_not_configured"
|
||||
description = "Agent App requires the Agent Soul model to be configured."
|
||||
code = 400
|
||||
|
||||
|
||||
class AgentSoulLockedError(BadRequest):
|
||||
description = "Agent Soul is locked for this workflow node."
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
These services keep product-facing locators (conversation, workflow run, node)
|
||||
on the API boundary and translate them into the agent backend's
|
||||
``SandboxLocator`` using persisted non-sensitive runtime layer specs plus the
|
||||
saved Agenton session snapshot.
|
||||
saved Agenton session snapshot. Upload responses stay console-facing here: the
|
||||
agent backend still returns a canonical ToolFile mapping, while this API layer
|
||||
re-resolves that mapping into a signed browser download URL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import urllib.parse
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from agenton.compositor import CompositorSessionSnapshot
|
||||
from dify_agent.client import Client
|
||||
@@ -18,7 +22,10 @@ from sqlalchemy import select
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.apps.agent_app.session_store import AgentAppRuntimeSessionStore
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime
|
||||
from core.db.session_factory import session_factory
|
||||
from factories import file_factory
|
||||
from models.agent import AgentRuntimeSessionOwnerType, WorkflowAgentRuntimeSession, WorkflowAgentRuntimeSessionStatus
|
||||
|
||||
_RUNTIME_LAYER_SPECS_ADAPTER: TypeAdapter[list[RuntimeLayerSpec]] = TypeAdapter(list[RuntimeLayerSpec])
|
||||
@@ -45,6 +52,12 @@ class AgentSandboxInfo(BaseModel):
|
||||
workspace_cwd: str
|
||||
|
||||
|
||||
class AgentSandboxUploadDownload(BaseModel):
|
||||
"""Signed browser download URL for one sandbox upload result."""
|
||||
|
||||
url: str
|
||||
|
||||
|
||||
class AgentAppSandboxService:
|
||||
"""Inspect and proxy file access for an Agent App conversation sandbox."""
|
||||
|
||||
@@ -77,9 +90,15 @@ class AgentAppSandboxService:
|
||||
locator = self._resolve_locator(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
||||
return self._client_factory().read_sandbox_file_sync(locator, path)
|
||||
|
||||
def upload_file(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str):
|
||||
def upload_file(
|
||||
self, *, tenant_id: str, app_id: str, conversation_id: str, path: str
|
||||
) -> AgentSandboxUploadDownload:
|
||||
locator = self._resolve_locator(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
||||
return self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
uploaded = self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
return _upload_download_response(
|
||||
tenant_id=tenant_id,
|
||||
file_mapping=uploaded.file.model_dump(mode="python"),
|
||||
)
|
||||
|
||||
def _resolve_locator(self, *, tenant_id: str, app_id: str, conversation_id: str) -> SandboxLocator:
|
||||
stored = self._session_store.load_active_session_for_conversation(
|
||||
@@ -153,7 +172,7 @@ class WorkflowAgentSandboxService:
|
||||
node_id: str,
|
||||
node_execution_id: str | None,
|
||||
path: str,
|
||||
):
|
||||
) -> AgentSandboxUploadDownload:
|
||||
locator = self._resolve_locator(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
@@ -161,7 +180,11 @@ class WorkflowAgentSandboxService:
|
||||
node_id=node_id,
|
||||
node_execution_id=node_execution_id,
|
||||
)
|
||||
return self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
uploaded = self._client_factory().upload_sandbox_file_sync(locator, path)
|
||||
return _upload_download_response(
|
||||
tenant_id=tenant_id,
|
||||
file_mapping=uploaded.file.model_dump(mode="python"),
|
||||
)
|
||||
|
||||
def _resolve_locator(
|
||||
self,
|
||||
@@ -246,6 +269,41 @@ def _deserialize_runtime_layer_specs(value: str | None) -> list[RuntimeLayerSpec
|
||||
return _RUNTIME_LAYER_SPECS_ADAPTER.validate_json(value)
|
||||
|
||||
|
||||
def _upload_download_response(*, tenant_id: str, file_mapping: dict[str, Any]) -> AgentSandboxUploadDownload:
|
||||
"""Resolve one uploaded ToolFile mapping into a signed external download URL."""
|
||||
|
||||
controller = DatabaseFileAccessController()
|
||||
runtime = DifyWorkflowFileRuntime(file_access_controller=controller)
|
||||
try:
|
||||
file = file_factory.build_from_mapping(
|
||||
mapping=file_mapping,
|
||||
tenant_id=tenant_id,
|
||||
access_controller=controller,
|
||||
)
|
||||
url = runtime.resolve_file_url(file=file, for_external=True)
|
||||
except ValueError as exc:
|
||||
raise AgentSandboxInspectorError(
|
||||
"sandbox_upload_download_unavailable",
|
||||
"uploaded sandbox file could not be converted to a download URL",
|
||||
status_code=502,
|
||||
) from exc
|
||||
|
||||
if not url:
|
||||
raise AgentSandboxInspectorError(
|
||||
"sandbox_upload_download_unavailable",
|
||||
"uploaded sandbox file does not support download URL generation",
|
||||
status_code=502,
|
||||
)
|
||||
return AgentSandboxUploadDownload(url=_with_as_attachment(url))
|
||||
|
||||
|
||||
def _with_as_attachment(url: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
|
||||
query.append(("as_attachment", "true"))
|
||||
return urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
|
||||
|
||||
|
||||
def _default_client_factory() -> Client:
|
||||
base_url = dify_config.AGENT_BACKEND_BASE_URL
|
||||
if not base_url:
|
||||
@@ -257,4 +315,10 @@ def _default_client_factory() -> Client:
|
||||
return Client(base_url=base_url)
|
||||
|
||||
|
||||
__all__ = ["AgentAppSandboxService", "AgentSandboxInfo", "AgentSandboxInspectorError", "WorkflowAgentSandboxService"]
|
||||
__all__ = [
|
||||
"AgentAppSandboxService",
|
||||
"AgentSandboxInfo",
|
||||
"AgentSandboxInspectorError",
|
||||
"AgentSandboxUploadDownload",
|
||||
"WorkflowAgentSandboxService",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user