mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
351 lines
16 KiB
Python
351 lines
16 KiB
Python
"""Inert native config composition. No ordinary session, credentials or model schema calls.
|
|
|
|
The bound public node and protected graph own selectors. Stored published Soul and
|
|
node-job config own declarations; the workspace service alone owns allocation.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Literal
|
|
|
|
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
|
|
from dify_agent.protocol.schemas import CreateRunRequest
|
|
from pydantic import JsonValue, TypeAdapter
|
|
from sqlalchemy import select
|
|
|
|
from clients.agent_backend.request_builder import (
|
|
AgentBackendModelConfig,
|
|
AgentBackendOutputConfig,
|
|
AgentBackendRunRequestBuilder,
|
|
AgentBackendWorkflowNodeRunInput,
|
|
)
|
|
from models.agent import (
|
|
Agent,
|
|
AgentConfigSnapshot,
|
|
WorkflowAgentBindingType,
|
|
WorkflowAgentNodeBinding,
|
|
)
|
|
from models.agent_config_entities import AgentSoulConfig, WorkflowNodeJobConfig
|
|
from runtime_contracts.builder.execution import (
|
|
BuilderExecutionPolicyError,
|
|
canonical_digest,
|
|
canonical_json,
|
|
)
|
|
from runtime_contracts.builder.native.agent_contracts import (
|
|
AgentParentNodeRef,
|
|
AgentTestWorkspaceRef,
|
|
)
|
|
from runtime_contracts.builder.native.contracts import FiniteJSON
|
|
from services.dify_builder.native_testing.agent_binding import (
|
|
AgentBindingService,
|
|
native_node_data,
|
|
)
|
|
from services.dify_builder.native_testing.agent_tool_declarations import (
|
|
AgentToolDeclarationService,
|
|
)
|
|
from services.dify_builder.native_testing.private_reads import (
|
|
validate_invocation_workflow,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from graphon.runtime import RuntimeState
|
|
from runtime_contracts.builder.native.runtime_contracts import (
|
|
ResolvedNativeTestContext,
|
|
)
|
|
from services.dify_builder.native_testing.repository import OwnedNativeTestLoader
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NativeAgentDefinition:
|
|
node_id: str
|
|
agent_id: str
|
|
snapshot_id: str
|
|
soul: AgentSoulConfig
|
|
node_job: WorkflowNodeJobConfig
|
|
stamp: str
|
|
|
|
|
|
class NativeAgentRuntimeConfigBuilder:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
loader: "OwnedNativeTestLoader",
|
|
declarations: AgentToolDeclarationService | None = None,
|
|
profile: Literal["isolated", "confirmed"] = "isolated",
|
|
):
|
|
self.loader = loader
|
|
self.declarations = declarations or AgentToolDeclarationService()
|
|
self.profile = profile
|
|
|
|
def load_definition(self, *, context: "ResolvedNativeTestContext", node_id: str) -> NativeAgentDefinition:
|
|
nodes = context.graph.get("nodes", [])
|
|
if not isinstance(nodes, list):
|
|
raise BuilderExecutionPolicyError("native_agent_owner_mismatch")
|
|
selected = [node for node in nodes if isinstance(node, dict) and node.get("id") == node_id]
|
|
if len(selected) != 1 or not isinstance(selected[0].get("data"), dict):
|
|
raise BuilderExecutionPolicyError("native_agent_owner_mismatch")
|
|
data = selected[0]["data"]
|
|
binding = data.get("agent_binding")
|
|
if (
|
|
data.get("type") != "agent"
|
|
or data.get("version") != "2"
|
|
or data.get("agent_node_kind") != "dify_agent"
|
|
or not isinstance(binding, dict)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_agent_owner_mismatch")
|
|
agent_id, snapshot_id = (
|
|
binding.get("agent_id"),
|
|
binding.get("current_snapshot_id"),
|
|
)
|
|
if not isinstance(agent_id, str) or not agent_id or not isinstance(snapshot_id, str) or not snapshot_id:
|
|
raise BuilderExecutionPolicyError("native_agent_pin_mismatch")
|
|
authorization = AgentBindingService.authorize(
|
|
actor=self.loader.actor,
|
|
app_id=context.app_id,
|
|
agent_ids=frozenset({agent_id}),
|
|
)
|
|
with self.loader.session_factory() as session:
|
|
AgentBindingService(session, authorization=authorization).recheck_access(
|
|
actor=self.loader.actor, app_id=context.app_id, agent_id=agent_id
|
|
)
|
|
workflow = validate_invocation_workflow(
|
|
session,
|
|
tenant_id=context.tenant_id,
|
|
app_id=context.app_id,
|
|
workflow_id=context.workflow_id,
|
|
)
|
|
if workflow._environment_variables not in (None, "[]", "{}", ""):
|
|
raise BuilderExecutionPolicyError("native_agent_environment_requires_owner")
|
|
row = session.scalar(
|
|
select(WorkflowAgentNodeBinding).where(
|
|
WorkflowAgentNodeBinding.tenant_id == context.tenant_id,
|
|
WorkflowAgentNodeBinding.app_id == context.app_id,
|
|
WorkflowAgentNodeBinding.workflow_id == context.workflow_id,
|
|
WorkflowAgentNodeBinding.workflow_version == workflow.version,
|
|
WorkflowAgentNodeBinding.node_id == node_id,
|
|
)
|
|
)
|
|
snapshot = session.get(AgentConfigSnapshot, snapshot_id)
|
|
agent = session.get(Agent, agent_id)
|
|
if (
|
|
row is None
|
|
or snapshot is None
|
|
or agent is None
|
|
or (
|
|
row.agent_id,
|
|
row.current_snapshot_id,
|
|
snapshot.tenant_id,
|
|
snapshot.agent_id,
|
|
agent.tenant_id,
|
|
agent.active_config_snapshot_id,
|
|
)
|
|
!= (
|
|
agent_id,
|
|
snapshot_id,
|
|
context.tenant_id,
|
|
agent_id,
|
|
context.tenant_id,
|
|
snapshot_id,
|
|
)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_agent_pin_mismatch")
|
|
if row.binding_type != WorkflowAgentBindingType.ROSTER_AGENT:
|
|
raise BuilderExecutionPolicyError("native_agent_pin_mismatch")
|
|
# Local visibility and publication are current; catalog performs no capability calls.
|
|
from core.agent.publish_visibility import (
|
|
workflow_callable_active_snapshot_filter,
|
|
)
|
|
from services.agent.roster_service import AgentRosterService
|
|
|
|
eligible = session.scalar(
|
|
AgentRosterService._build_roster_agents_stmt(tenant_id=context.tenant_id, keyword=None).where(
|
|
Agent.id == agent_id,
|
|
Agent.active_config_has_model.is_(True),
|
|
workflow_callable_active_snapshot_filter(),
|
|
)
|
|
)
|
|
if eligible is None:
|
|
raise BuilderExecutionPolicyError("agent_access_denied")
|
|
soul = AgentSoulConfig.model_validate(snapshot.config_snapshot_dict)
|
|
job = WorkflowNodeJobConfig.model_validate(row.node_job_config_dict)
|
|
expected_data = native_node_data(agent_id=agent_id, snapshot_id=snapshot_id, node_job=job)
|
|
if any(data.get(key) != value for key, value in expected_data.items() if key != "title"):
|
|
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
|
|
stamp = canonical_digest(
|
|
{
|
|
"node": selected[0],
|
|
"soul": soul.model_dump(mode="json"),
|
|
"job": job.model_dump(mode="json"),
|
|
}
|
|
)
|
|
return NativeAgentDefinition(node_id, agent_id, snapshot_id, soul, job, stamp)
|
|
|
|
@staticmethod
|
|
def typed_inputs(*, runtime_state: "RuntimeState", definition: NativeAgentDefinition) -> dict:
|
|
from core.workflow.system_variables import SystemVariableKey, get_system_value
|
|
from services.agent.prompt_mentions import (
|
|
extract_workflow_node_output_selectors,
|
|
normalize_previous_node_output_selector,
|
|
)
|
|
|
|
pool = runtime_state.variable_pool
|
|
selectors = list(extract_workflow_node_output_selectors(definition.node_job.workflow_prompt))
|
|
if definition.node_job.output_routes.enabled:
|
|
for route in definition.node_job.output_routes.routes:
|
|
selectors.extend(extract_workflow_node_output_selectors(route.name))
|
|
for ref in definition.node_job.previous_node_output_refs:
|
|
selector = normalize_previous_node_output_selector(ref)
|
|
if selector is None:
|
|
raise BuilderExecutionPolicyError("native_agent_input_unavailable")
|
|
selectors.append(selector)
|
|
values = {}
|
|
for selector in selectors:
|
|
if not selector:
|
|
raise BuilderExecutionPolicyError("native_agent_input_unavailable")
|
|
segment = pool.get(selector)
|
|
if segment is None:
|
|
raise BuilderExecutionPolicyError("native_agent_input_unavailable")
|
|
values[".".join(selector)] = TypeAdapter(FiniteJSON).validate_python(segment.value)
|
|
files = get_system_value(pool, SystemVariableKey.FILES)
|
|
if files:
|
|
raise BuilderExecutionPolicyError("native_input_import_requires_capability")
|
|
query = get_system_value(pool, SystemVariableKey.QUERY)
|
|
from graphon.variables.template_resolution import convert_template
|
|
|
|
route_names = (
|
|
{route.id: convert_template(pool, route.name).text for route in definition.node_job.output_routes.routes}
|
|
if definition.node_job.output_routes.enabled
|
|
else {}
|
|
)
|
|
return TypeAdapter(FiniteJSON).validate_python({"query": query, "values": values, "route_names": route_names})
|
|
|
|
def compose(
|
|
self,
|
|
*,
|
|
context: "ResolvedNativeTestContext",
|
|
definition: NativeAgentDefinition,
|
|
parent_node: AgentParentNodeRef,
|
|
workspace: AgentTestWorkspaceRef,
|
|
inputs: dict,
|
|
) -> CreateRunRequest:
|
|
from core.plugin.provider_identity import (
|
|
normalize_plugin_daemon_provider_identity,
|
|
)
|
|
from core.workflow.nodes.agent_v2.runtime_request_builder import (
|
|
WorkflowAgentRuntimeRequestBuilder,
|
|
build_config_layer_config,
|
|
build_knowledge_layer_config,
|
|
build_shell_layer_config,
|
|
)
|
|
from models.provider_ids import ModelProviderID
|
|
from services.agent.prompt_mentions import (
|
|
build_soul_mention_resolver,
|
|
expand_prompt_mentions,
|
|
)
|
|
|
|
soul, job = definition.soul, definition.node_job
|
|
if soul.model is None:
|
|
raise BuilderExecutionPolicyError("native_agent_model_unavailable")
|
|
# Every shell env owner must be reference-only before ordinary helpers map
|
|
# name/value pairs (which no recursive dictionary-key filter can identify).
|
|
for env in (soul.env, *(tool.env for tool in soul.tools.cli_tools)):
|
|
if any(item.value is not None or item.default is not None for item in env.variables):
|
|
raise BuilderExecutionPolicyError("native_agent_environment_requires_owner")
|
|
if any(secret.get("value") is not None or secret.get("default") is not None for secret in env.secret_refs):
|
|
raise BuilderExecutionPolicyError("native_agent_config_credentials")
|
|
declarations = self.declarations.prepare(
|
|
tenant_id=context.tenant_id,
|
|
actor_id=context.actor_id,
|
|
app_id=context.app_id,
|
|
tools=soul.tools,
|
|
profile=self.profile,
|
|
)
|
|
plugin_id, provider = normalize_plugin_daemon_provider_identity(
|
|
ModelProviderID(soul.model.model_provider), soul.model.plugin_id
|
|
)
|
|
config, warnings = build_config_layer_config(
|
|
soul, agent_id=definition.agent_id, config_version_id=definition.snapshot_id
|
|
)
|
|
if warnings:
|
|
raise BuilderExecutionPolicyError("native_agent_config_asset_unavailable")
|
|
job.output_routes.validate_for_execution()
|
|
output = WorkflowAgentRuntimeRequestBuilder._build_output_config(job.declared_outputs)
|
|
if job.output_routes.enabled:
|
|
route_names = inputs.get("route_names")
|
|
if not isinstance(route_names, dict) or set(route_names) != {
|
|
route.id for route in job.output_routes.routes
|
|
}:
|
|
raise BuilderExecutionPolicyError("native_agent_input_unavailable")
|
|
schema: dict[str, JsonValue] = (
|
|
output.json_schema if output else {"type": "object", "properties": {"text": {"type": "string"}}}
|
|
)
|
|
properties = schema.get("properties")
|
|
required = schema.get("required", [])
|
|
if not isinstance(properties, dict) or not isinstance(required, list):
|
|
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
|
|
properties["switch"] = {
|
|
"type": "string",
|
|
"enum": [route.id for route in job.output_routes.routes],
|
|
"description": "Select exactly one route ID based on the task result:\n"
|
|
+ "\n".join(
|
|
f"{route.id} ({route.label or route.id}): {route_names[route.id]}"
|
|
for route in job.output_routes.routes
|
|
),
|
|
}
|
|
schema["required"] = [*required, "switch"]
|
|
output = AgentBackendOutputConfig(json_schema=schema, description=output.description if output else None)
|
|
request = AgentBackendRunRequestBuilder().build_for_workflow_node(
|
|
AgentBackendWorkflowNodeRunInput(
|
|
model=AgentBackendModelConfig(
|
|
plugin_id=plugin_id,
|
|
model_provider=provider,
|
|
model=soul.model.model,
|
|
model_settings=soul.model.model_settings.model_dump(mode="json", exclude_none=True),
|
|
),
|
|
execution_context=DifyExecutionContextLayerConfig(
|
|
tenant_id=context.tenant_id,
|
|
user_id=context.actor_id,
|
|
user_from="account",
|
|
app_id=context.app_id,
|
|
workflow_id=context.workflow_id,
|
|
workflow_run_id=context.native_run_id,
|
|
node_id=definition.node_id,
|
|
node_execution_id=parent_node.execution_id,
|
|
agent_id=definition.agent_id,
|
|
agent_config_version_id=definition.snapshot_id,
|
|
agent_config_version_kind="snapshot",
|
|
agent_mode="workflow_run",
|
|
invoke_from="debugger",
|
|
),
|
|
backend_binding_ref=workspace.backend_binding_ref,
|
|
agent_soul_prompt=expand_prompt_mentions(soul.prompt.system_prompt, build_soul_mention_resolver(soul)),
|
|
workflow_node_job_prompt=job.workflow_prompt or "Complete the declared workflow task.",
|
|
user_prompt=canonical_json(inputs).decode(),
|
|
output=output,
|
|
tools=declarations.plugin_tools,
|
|
core_tools=declarations.core_tools,
|
|
knowledge=build_knowledge_layer_config(soul),
|
|
config_layer_config=config,
|
|
include_shell=True,
|
|
shell_config=build_shell_layer_config(soul),
|
|
)
|
|
)
|
|
return CreateRunRequest.model_validate(
|
|
{
|
|
**request.model_dump(mode="json"),
|
|
"execution_mode": "builder_test",
|
|
"native_test_profile": self.profile,
|
|
"idempotency_key": canonical_digest(
|
|
{
|
|
"parent": parent_node.model_dump(mode="json"),
|
|
"snapshot": definition.snapshot_id,
|
|
"profile": self.profile,
|
|
}
|
|
)
|
|
if self.profile == "confirmed"
|
|
else request.idempotency_key,
|
|
"native_test_ref": parent_node.ref.model_dump(mode="json"),
|
|
"native_parent_node": parent_node.model_dump(mode="json"),
|
|
"native_test_protocol_version": 1,
|
|
}
|
|
)
|