mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 23:01:11 +08:00
Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2
This commit is contained in:
commit
46de0d78d9
@ -36,8 +36,8 @@ class AgentBackendConfig(BaseSettings):
|
||||
description=(
|
||||
"Inject the dify.drive layer (Skills & Files drive manifest declaration) "
|
||||
"into Agent runs. The declaration is an index only — the agent backend "
|
||||
"pulls the actual SKILL.md / files through the back proxy. Keep it off "
|
||||
"until the agent backend registers the dify.drive layer type."
|
||||
"pulls the actual SKILL.md / files through the back proxy. Set this to "
|
||||
"false only when temporarily rolling back the drive integration."
|
||||
),
|
||||
default=False,
|
||||
default=True,
|
||||
)
|
||||
|
||||
@ -128,7 +128,7 @@ class TestAgentAppRuntimeRequestBuilder:
|
||||
req = result.request
|
||||
assert req.purpose == "agent_app"
|
||||
names = [layer.name for layer in req.composition.layers]
|
||||
assert names == ["agent_soul_prompt", "agent_app_user_prompt", "execution_context", "history", "llm"]
|
||||
assert names == ["agent_soul_prompt", "agent_app_user_prompt", "execution_context", "drive", "history", "llm"]
|
||||
# plugin_id / provider normalized for plugin-daemon transport.
|
||||
llm = next(layer for layer in req.composition.layers if layer.name == "llm")
|
||||
assert llm.config.plugin_id == "langgenius/openai"
|
||||
@ -251,9 +251,26 @@ class TestAgentAppRuntimeRequestBuilder:
|
||||
|
||||
|
||||
def _soul_with_model_and_skill() -> AgentSoulConfig:
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = "Use [§skill:tender-analyzer%2FSKILL.md:Tender Analyzer§]"
|
||||
return soul
|
||||
return AgentSoulConfig.model_validate(
|
||||
{
|
||||
"model": {
|
||||
"plugin_id": "langgenius/openai",
|
||||
"model_provider": "langgenius/openai/openai",
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
"prompt": {"system_prompt": "Use [§skill:tender-analyzer%2FSKILL.md:Tender Analyzer§]"},
|
||||
"files": {
|
||||
"skills": [
|
||||
{
|
||||
"path": "tender-analyzer",
|
||||
"skill_md_key": "tender-analyzer/SKILL.md",
|
||||
"name": "Tender Analyzer",
|
||||
"description": "Parses RFPs.",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TestAgentAppDriveLayer:
|
||||
@ -261,28 +278,6 @@ class TestAgentAppDriveLayer:
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.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: [
|
||||
{
|
||||
"path": "tender-analyzer",
|
||||
"skill_md_key": "tender-analyzer/SKILL.md",
|
||||
"archive_key": None,
|
||||
"name": "Tender Analyzer",
|
||||
"description": "Parses RFPs.",
|
||||
"size": 1,
|
||||
"mime_type": "text/markdown",
|
||||
"hash": None,
|
||||
"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}
|
||||
],
|
||||
)
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
plugin_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
@ -305,14 +300,6 @@ class TestAgentAppDriveLayer:
|
||||
"core.app.apps.agent_app.runtime_request_builder.dify_config.AGENT_DRIVE_MANIFEST_ENABLED", True
|
||||
)
|
||||
monkeypatch.setattr("core.app.apps.agent_app.runtime_request_builder.dify_config.AGENT_SHELL_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: [],
|
||||
)
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
plugin_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
@ -328,7 +315,10 @@ class TestAgentAppDriveLayer:
|
||||
"drive": "drive",
|
||||
}
|
||||
|
||||
def test_no_drive_layer_when_flag_disabled(self):
|
||||
def test_no_drive_layer_when_flag_disabled(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.runtime_request_builder.dify_config.AGENT_DRIVE_MANIFEST_ENABLED", False
|
||||
)
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
plugin_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
@ -343,29 +333,6 @@ class TestAgentAppDriveLayer:
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.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: [
|
||||
{
|
||||
"path": "tender-analyzer",
|
||||
"skill_md_key": "tender-analyzer/SKILL.md",
|
||||
"archive_key": None,
|
||||
"name": "Tender Analyzer",
|
||||
"description": "Parses RFPs.",
|
||||
"size": 1,
|
||||
"mime_type": "text/markdown",
|
||||
"hash": None,
|
||||
"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": "files/sample.pdf", "is_skill": False},
|
||||
],
|
||||
)
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = (
|
||||
"Use [§skill:tender-analyzer%2FSKILL.md:Tender Analyzer§] and [§file:files%2Fsample.pdf:sample.pdf§]."
|
||||
@ -388,14 +355,6 @@ class TestAgentAppDriveLayer:
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.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: [],
|
||||
)
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = (
|
||||
"Use [§skill:ghost%2FSKILL.md:Ghost Skill§], [§file:files%2Fghost.txt:Ghost File§], "
|
||||
@ -416,29 +375,6 @@ class TestAgentAppDriveLayer:
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.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: [
|
||||
{
|
||||
"path": "tender-analyzer",
|
||||
"skill_md_key": "tender-analyzer/SKILL.md",
|
||||
"archive_key": None,
|
||||
"name": "Tender Analyzer",
|
||||
"description": "Parses RFPs.",
|
||||
"size": 1,
|
||||
"mime_type": "text/markdown",
|
||||
"hash": None,
|
||||
"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": "files/sample.pdf", "is_skill": False},
|
||||
],
|
||||
)
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = (
|
||||
"Use [§skill:tender-analyzer%2FSKILL.md:Tender Analyzer§] and [§file:files%2Fsample.pdf:sample.pdf§]"
|
||||
@ -461,14 +397,6 @@ class TestAgentAppDriveLayer:
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.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: [],
|
||||
)
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = (
|
||||
"Use [§skill:ghost%2FSKILL.md:Ghost Skill§], [§file:files%2Fghost.txt:Ghost File§], "
|
||||
|
||||
@ -181,7 +181,8 @@ def test_builds_create_run_request_from_agent_soul_and_node_job():
|
||||
assert "Previous result" in dumped["composition"]["layers"][2]["config"]["user"]
|
||||
assert dumped["composition"]["layers"][-1]["config"]["json_schema"]["properties"]["summary"]["type"] == "string"
|
||||
assert DIFY_AGENT_HISTORY_LAYER_ID in layers
|
||||
assert result.redacted_request["composition"]["layers"][5]["config"]["credentials"] == "[REDACTED]"
|
||||
redacted_layers = {layer["name"]: layer for layer in result.redacted_request["composition"]["layers"]}
|
||||
assert redacted_layers[DIFY_AGENT_MODEL_LAYER_ID]["config"]["credentials"] == "[REDACTED]"
|
||||
|
||||
|
||||
def test_normalizes_langgenius_model_provider_for_agent_backend_transport():
|
||||
@ -262,7 +263,7 @@ def test_builds_workflow_run_request_with_file_output_schema_and_reserved_metada
|
||||
assert report_schema["oneOf"][3]["required"] == ["transfer_method", "url"]
|
||||
assert output_schema["properties"]["confidence"]["type"] == "number"
|
||||
assert output_schema["required"] == ["report"]
|
||||
assert dumped["composition"]["layers"][5]["config"]["model_settings"] == {"temperature": 0.2}
|
||||
assert layers[DIFY_AGENT_MODEL_LAYER_ID]["config"]["model_settings"] == {"temperature": 0.2}
|
||||
assert result.metadata["runtime_support"]["reserved_status"]["tools.dify_tools"] == "supported_when_config_valid"
|
||||
assert result.metadata["runtime_support"]["reserved_status"]["tools.cli_tools"] == "supported_by_shell_bootstrap"
|
||||
assert result.metadata["runtime_support"]["unsupported_runtime_warnings"] == []
|
||||
@ -1103,11 +1104,14 @@ def test_workflow_runtime_missing_drive_mentions_fall_back_to_label_then_decoded
|
||||
result = WorkflowAgentRuntimeRequestBuilder(credentials_provider=FakeCredentialsProvider()).build(context)
|
||||
|
||||
soul_prompt = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
|
||||
assert soul_prompt.config.prefix == "Use Ghost Skill, Ghost File, and files/no-label.txt."
|
||||
assert soul_prompt.config.prefix == "Use Ghost Skill, Ghost File, and no-label.txt."
|
||||
assert "[§" not in soul_prompt.config.prefix
|
||||
|
||||
|
||||
def test_workflow_run_request_has_no_drive_layer_when_flag_disabled():
|
||||
def test_workflow_run_request_has_no_drive_layer_when_flag_disabled(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.workflow.nodes.agent_v2.runtime_request_builder.dify_config.AGENT_DRIVE_MANIFEST_ENABLED", False
|
||||
)
|
||||
context = _context()
|
||||
context.snapshot.config_snapshot = _soul_with_drive_skill()
|
||||
|
||||
|
||||
@ -5,7 +5,10 @@ mentioned in the prompt. When the layer enters a run context it eagerly pulls
|
||||
those mentioned skills/files from the Dify inner drive bridge, materializes them
|
||||
under the fixed Agent Stub drive base for ``drive_ref``, and contributes a
|
||||
concise prompt block describing what was loaded and what other skills remain
|
||||
available for lazy pull.
|
||||
available for lazy pull. It also contributes a suffix prompt with
|
||||
``dify-agent drive`` and ``dify-agent file`` usage so the model has concrete
|
||||
Agent Stub commands for materializing drive content and workflow files when a
|
||||
shell layer is available.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -27,6 +30,26 @@ from dify_agent.layers.drive.configs import DIFY_DRIVE_LAYER_TYPE_ID, DifyDriveL
|
||||
|
||||
_SKILL_ARCHIVE_FILENAME = ".DIFY-SKILL-FULL.zip"
|
||||
_DOWNLOAD_CONCURRENCY = 4
|
||||
_AGENT_STUB_CLI_USAGE_PROMPT = """Agent Stub CLI usage is available inside shell jobs:
|
||||
|
||||
Drive commands:
|
||||
|
||||
- List drive items: `dify-agent drive list [PATH_PREFIX]`
|
||||
- Emit the drive manifest as JSON: `dify-agent drive list [PATH_PREFIX] --json`
|
||||
- Pull drive keys or prefixes: `dify-agent drive pull TARGET ...`
|
||||
Pulled files are written under `$DIFY_AGENT_STUB_DRIVE_BASE` by default.
|
||||
Use `--drive-base .` to materialize pulled files under the current working directory.
|
||||
- Upload a local file or directory: `dify-agent drive push LOCAL_PATH DRIVE_PATH`
|
||||
Add `--recursive` to upload raw directory contents. Without `--recursive`, a directory must contain `SKILL.md`
|
||||
and is uploaded as a standardized skill.
|
||||
|
||||
File commands:
|
||||
|
||||
- Download one workflow file mapping: `dify-agent file download TRANSFER_METHOD REFERENCE_OR_URL [DIR]`
|
||||
`TRANSFER_METHOD` is one of `local_file`, `tool_file`, `datasource_file`, or `remote_url`.
|
||||
If `DIR` is omitted, the file is saved in the current working directory.
|
||||
- Upload one sandbox-local output file: `dify-agent file upload PATH`
|
||||
The command prints a JSON file mapping such as `{"transfer_method":"tool_file","reference":"..."}`."""
|
||||
|
||||
|
||||
class DifyDriveLayerError(RuntimeError):
|
||||
@ -81,6 +104,11 @@ class DifyDriveLayer(PlainLayer[DifyDriveDeps, DifyDriveLayerConfig, EmptyRuntim
|
||||
def prefix_prompts(self) -> list[str]:
|
||||
return [self.build_prompt_context()]
|
||||
|
||||
@property
|
||||
@override
|
||||
def suffix_prompts(self) -> list[str]:
|
||||
return [_AGENT_STUB_CLI_USAGE_PROMPT]
|
||||
|
||||
@override
|
||||
async def on_context_create(self) -> None:
|
||||
await self._pull_mentioned_targets()
|
||||
@ -122,9 +150,6 @@ class DifyDriveLayer(PlainLayer[DifyDriveDeps, DifyDriveLayerConfig, EmptyRuntim
|
||||
|
||||
if not sections:
|
||||
return ""
|
||||
sections.append(
|
||||
"Additional drive skills/files can be pulled lazily later with the Agent Stub drive commands if needed."
|
||||
)
|
||||
return "\n\n".join(sections)
|
||||
|
||||
async def _pull_mentioned_targets(self) -> None:
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
import pytest
|
||||
|
||||
from agenton.layers import EmptyRuntimeState, LayerConfig, NoLayerDeps, PlainLayer
|
||||
@ -15,10 +16,15 @@ class _FakeExecutionContextConfig(LayerConfig):
|
||||
|
||||
|
||||
class _FakeExecutionContextLayer(PlainLayer[NoLayerDeps, _FakeExecutionContextConfig, EmptyRuntimeState]):
|
||||
type_id = None
|
||||
type_id: ClassVar[str | None] = None
|
||||
|
||||
def __init__(self, tenant_id: str) -> None:
|
||||
self.config = _FakeExecutionContextConfig(tenant_id=tenant_id)
|
||||
config: _FakeExecutionContextConfig
|
||||
|
||||
def __new__(cls) -> _FakeExecutionContextLayer:
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config = _FakeExecutionContextConfig(tenant_id="tenant-1")
|
||||
|
||||
|
||||
def _build_layer(tmp_path: Path) -> DifyDriveLayer:
|
||||
@ -47,10 +53,24 @@ def _build_layer(tmp_path: Path) -> DifyDriveLayer:
|
||||
inner_api_url="https://api.example.com",
|
||||
inner_api_key="secret",
|
||||
)
|
||||
layer.bind_deps({"execution_context": _FakeExecutionContextLayer("tenant-1")})
|
||||
layer.bind_deps({"execution_context": _FakeExecutionContextLayer()})
|
||||
return layer
|
||||
|
||||
|
||||
def test_drive_layer_exposes_agent_stub_cli_usage_suffix_prompt(tmp_path: Path) -> None:
|
||||
layer = _build_layer(tmp_path)
|
||||
|
||||
assert len(layer.suffix_prompts) == 1
|
||||
prompt = layer.suffix_prompts[0]
|
||||
assert "dify-agent drive list [PATH_PREFIX]" in prompt
|
||||
assert "dify-agent drive pull TARGET ..." in prompt
|
||||
assert "--drive-base ." in prompt
|
||||
assert "dify-agent drive push LOCAL_PATH DRIVE_PATH" in prompt
|
||||
assert "dify-agent file download TRANSFER_METHOD REFERENCE_OR_URL [DIR]" in prompt
|
||||
assert "dify-agent file upload PATH" in prompt
|
||||
assert '{"transfer_method":"tool_file","reference":"..."}' in prompt
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_on_context_create_loads_mentioned_targets_into_prompt(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
|
||||
Loading…
Reference in New Issue
Block a user