mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
272 lines
10 KiB
Python
272 lines
10 KiB
Python
"""Legacy deterministic ``DifyBuilderAgent`` used by isolated domain tests.
|
|
|
|
Port of dify-enterprise/server/pkg/enterprise/biz/dify_builder/placeholder_agent.go.
|
|
|
|
It is not selectable by production configuration; runtime Builder cognition is
|
|
always provided by ``LlmBuilderAgent``. The deterministic implementation stays
|
|
as a compact fixture for state-machine tests that do not exercise model calls.
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from core.dify_builder.contract import ResourceOption
|
|
from core.dify_builder.models import (
|
|
ChecklistError,
|
|
ConversationItem,
|
|
Diagnosis,
|
|
DifyBuilderContext,
|
|
Graph,
|
|
Inputs,
|
|
MutationIntent,
|
|
NodeOutput,
|
|
Risk,
|
|
Run,
|
|
StartSchema,
|
|
)
|
|
from core.dify_builder.state import PcState
|
|
from graphon.enums import BuiltinNodeTypes
|
|
from services.dify_builder import node_defaults
|
|
|
|
__all__ = [
|
|
"BUILD_END_ID",
|
|
"BUILD_KNOWLEDGE_ID",
|
|
"BUILD_LLM_ID",
|
|
"BUILD_START_ID",
|
|
"FIXED_CODE",
|
|
"PlaceholderAgent",
|
|
]
|
|
|
|
FIXED_CODE = 'def main() -> dict:\n return {"result": "ok"}'
|
|
|
|
# Deterministic node ids for the canned Build graph, so connect intents can
|
|
# reference them and built_node_ids is known up front.
|
|
BUILD_START_ID = "start"
|
|
BUILD_KNOWLEDGE_ID = "knowledge_retrieval"
|
|
BUILD_LLM_ID = "llm"
|
|
BUILD_END_ID = "end"
|
|
|
|
|
|
class PlaceholderAgent:
|
|
"""Deterministic test cognition; see module docstring."""
|
|
|
|
def diagnose(self, failed_run: Run, graph: Graph, node_outputs: list[NodeOutput]) -> Diagnosis:
|
|
culprit = "output"
|
|
for o in node_outputs:
|
|
if o.status in ("failed", "exception"):
|
|
culprit = o.node_id
|
|
break
|
|
return Diagnosis(culprit_node_id=culprit, root_cause="Code node raised at runtime", severity="high")
|
|
|
|
def diagnose_checklist(self, errors: list[ChecklistError], graph: Graph) -> Diagnosis:
|
|
culprit, cause = "", "Checklist error"
|
|
for e in errors:
|
|
if e.messages:
|
|
culprit, cause = e.node_id, e.messages[0]
|
|
break
|
|
return Diagnosis(culprit_node_id=culprit, root_cause=cause, severity="medium")
|
|
|
|
def propose_repair(self, diagnosis: Diagnosis, graph: Graph) -> tuple[list[MutationIntent], Risk]:
|
|
intents = [
|
|
MutationIntent(
|
|
op="set_node_config",
|
|
args={"node_id": diagnosis.culprit_node_id, "path": "code", "value": FIXED_CODE},
|
|
)
|
|
]
|
|
return intents, Risk(level="low", reason="config-only fix", has_external_side_effect=False)
|
|
|
|
def generate_mock_inputs(self, schema: StartSchema, prior_failed: Inputs) -> Inputs:
|
|
return {"query": "mock"}
|
|
|
|
# -- Build cognition (Slice 2; fixed, deterministic canned output) --
|
|
|
|
def analyze_goal(self, goal_text: str) -> dict[str, Any]:
|
|
# Always proceeds -- no real missing-info branch. The challenge card
|
|
# the handler shows is an informational "proceeding" note.
|
|
return {
|
|
"fields": [
|
|
{"key": "report_types", "label": "Report types", "type": "text"},
|
|
{"key": "audience", "label": "Audience", "type": "text"},
|
|
{"key": "currency", "label": "Currency", "type": "text"},
|
|
{"key": "metrics", "label": "Metrics", "type": "text"},
|
|
{"key": "output", "label": "Output", "type": "textarea"},
|
|
{"key": "prefer_audited", "label": "Prefer audited sources", "type": "bool"},
|
|
],
|
|
"values": {
|
|
"report_types": "quarterly",
|
|
"audience": "executives",
|
|
"currency": "USD",
|
|
"metrics": "revenue, gross_margin",
|
|
"output": "PDF summary",
|
|
"prefer_audited": True,
|
|
},
|
|
}
|
|
|
|
def propose_plan_v1(self, requirements: dict[str, Any]) -> list[str]:
|
|
return [
|
|
"Ingest source documents",
|
|
"Retrieve relevant knowledge",
|
|
"Summarize with an LLM",
|
|
"Emit the final report",
|
|
]
|
|
|
|
def discover_resources(self, plan_items: list[str]) -> list[ResourceOption]:
|
|
return [
|
|
ResourceOption(
|
|
id="kb-company",
|
|
label="Company Knowledge Base",
|
|
meta="1,024 documents",
|
|
kind="knowledge",
|
|
readiness="ready",
|
|
)
|
|
]
|
|
|
|
def bind_resources(self, plan_items: list[str], resource_ids: list[str], conflict_policy: str) -> list[str]:
|
|
return [
|
|
"Ingest source documents",
|
|
"Retrieve from Company Knowledge Base",
|
|
"Summarize with the configured LLM",
|
|
"Emit the final report",
|
|
]
|
|
|
|
def build_nodes(self, plan_items: list[str]) -> list[MutationIntent]:
|
|
# Start -> Knowledge-Retrieval -> LLM -> End. Creates and connects are
|
|
# interleaved so each connect's endpoints already exist when
|
|
# apply_connect validates them.
|
|
return [
|
|
MutationIntent(
|
|
op="create_node",
|
|
args={
|
|
"node_type": BuiltinNodeTypes.START,
|
|
"config": node_defaults.default_config(BuiltinNodeTypes.START),
|
|
"node_id": BUILD_START_ID,
|
|
},
|
|
),
|
|
MutationIntent(
|
|
op="create_node",
|
|
args={
|
|
"node_type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL,
|
|
"config": node_defaults.default_config(BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL),
|
|
"node_id": BUILD_KNOWLEDGE_ID,
|
|
},
|
|
),
|
|
MutationIntent(op="connect", args={"from_node": BUILD_START_ID, "to_node": BUILD_KNOWLEDGE_ID}),
|
|
MutationIntent(
|
|
op="create_node",
|
|
args={
|
|
"node_type": BuiltinNodeTypes.LLM,
|
|
"config": node_defaults.default_config(BuiltinNodeTypes.LLM),
|
|
"node_id": BUILD_LLM_ID,
|
|
},
|
|
),
|
|
MutationIntent(op="connect", args={"from_node": BUILD_KNOWLEDGE_ID, "to_node": BUILD_LLM_ID}),
|
|
MutationIntent(
|
|
op="create_node",
|
|
args={
|
|
"node_type": BuiltinNodeTypes.END,
|
|
"config": node_defaults.default_config(BuiltinNodeTypes.END),
|
|
"node_id": BUILD_END_ID,
|
|
},
|
|
),
|
|
MutationIntent(op="connect", args={"from_node": BUILD_LLM_ID, "to_node": BUILD_END_ID}),
|
|
]
|
|
|
|
def learn_from_build(
|
|
self,
|
|
goal_text: str,
|
|
requirements: dict[str, Any],
|
|
plan_items: list[str],
|
|
built_node_ids: list[str],
|
|
) -> str:
|
|
return "Reusable skill: a Start->Knowledge->LLM->End summary workflow"
|
|
|
|
# -- Edit cognition (Slice 3; fixed, deterministic canned output) --
|
|
|
|
def analyze_impact(self, goal_text: str, graph: Graph) -> dict[str, Any]:
|
|
# Pick affected nodes from the existing graph (LLM + knowledge), with a
|
|
# canned-id fallback so the flow works on any Build-shaped draft.
|
|
node_ids = {n.get("id") for n in graph.get("nodes", [])}
|
|
targets = [nid for nid in (BUILD_LLM_ID, BUILD_KNOWLEDGE_ID) if nid in node_ids] or [BUILD_LLM_ID]
|
|
return {
|
|
"fields": [
|
|
{"key": "risk_threshold", "label": "Risk threshold", "type": "text"},
|
|
{
|
|
"key": "review_team",
|
|
"label": "Review team",
|
|
"type": "select",
|
|
"options": ["compliance", "legal", "engineering"],
|
|
},
|
|
{
|
|
"key": "timeout_behavior",
|
|
"label": "Timeout behavior",
|
|
"type": "select",
|
|
"options": ["fail_open", "fail_closed"],
|
|
},
|
|
{"key": "preserve_summary", "label": "Preserve summary", "type": "bool"},
|
|
],
|
|
"values": {
|
|
"risk_threshold": "medium",
|
|
"review_team": "compliance",
|
|
"timeout_behavior": "fail_closed",
|
|
"preserve_summary": True,
|
|
},
|
|
"target_node_ids": targets,
|
|
}
|
|
|
|
def propose_edit_plan(self, edit_rules: dict[str, Any], graph: Graph) -> list[str]:
|
|
return [
|
|
"Tighten the LLM risk threshold",
|
|
"Route high-risk output to the review team",
|
|
"Apply the timeout behavior",
|
|
"Preserve the existing summary contract",
|
|
]
|
|
|
|
def build_edit_intents(self, edit_rules: dict[str, Any], graph: Graph) -> list[MutationIntent]:
|
|
# Canned config-level edits (set_node_config) on the existing LLM node,
|
|
# matching the edit_rules semantics. Deterministic target selection:
|
|
# prefer the canned LLM id, else the lexicographically-first node id.
|
|
node_ids = {n.get("id") for n in graph.get("nodes", []) if n.get("id")}
|
|
if BUILD_LLM_ID in node_ids:
|
|
llm_id = BUILD_LLM_ID
|
|
elif node_ids:
|
|
llm_id = min(node_ids)
|
|
else:
|
|
llm_id = BUILD_LLM_ID
|
|
return [
|
|
MutationIntent(
|
|
op="set_node_config",
|
|
args={
|
|
"node_id": llm_id,
|
|
"path": "risk_threshold",
|
|
"value": edit_rules.get("risk_threshold", "medium"),
|
|
},
|
|
),
|
|
MutationIntent(
|
|
op="set_node_config",
|
|
args={
|
|
"node_id": llm_id,
|
|
"path": "timeout_behavior",
|
|
"value": edit_rules.get("timeout_behavior", "fail_closed"),
|
|
},
|
|
),
|
|
]
|
|
|
|
def respond_to_message(
|
|
self,
|
|
state: PcState,
|
|
context: DifyBuilderContext,
|
|
history: list[ConversationItem],
|
|
graph: Graph,
|
|
text: str,
|
|
on_delta: Callable[[str], None] | None = None,
|
|
) -> str:
|
|
"""Return a deterministic reply for isolated state-machine tests."""
|
|
del context, history, graph
|
|
reply = (
|
|
f'I understand your note: "{text}". The Builder is currently at {state}. '
|
|
"I have not changed the workflow; use one of the available actions when you want to continue."
|
|
)
|
|
if on_delta is not None:
|
|
on_delta(reply)
|
|
return reply
|