feat(dify-builder): build_nodes via WorkflowGenerator with model+dataset grounding

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Charles YaoandClaude Opus 4.8 committed 2026-08-29 12:04:58 -07:00
1 parent 0ba1f932a9
commit dc85ea51a2
2 files changed
+142 -1

No files matched your search

+70 -1
View File
@@ -7,8 +7,14 @@ rather than crashing the advance. build_nodes lives in the same module
from typing import Any
from core.app.app_config.entities import ModelConfig
from core.dify_builder.contract import ResourceOption
from services.dify_builder.agent import llm, resources
from core.dify_builder.models import MutationIntent
from graphon.enums import BUILT_IN_NODE_TYPES
from services.dify_builder import graph_ops
from services.dify_builder.agent import graph_translate, llm, resources
from services.dify_builder.agent.model_resolver import resolve_model_instance
from services.workflow_generator_service import WorkflowGeneratorService
def analyze_goal(model, goal_text: str) -> dict[str, Any]:
@@ -115,3 +121,66 @@ def learn_from_build(
return llm.invoke_text(model, system=system, user=f"GOAL: {goal_text}\nPLAN: {plan_items}") or fallback
except Exception:
return fallback
_ALLOWED_NODE_TYPES: set[str] = set(BUILT_IN_NODE_TYPES)
def _generator_model_config(tenant_id: str, model_config: dict[str, Any]) -> ModelConfig:
if model_config:
return ModelConfig.model_validate({
"provider": model_config.get("provider", ""),
"name": model_config.get("name", ""),
"mode": model_config.get("mode", "chat"),
"completion_params": model_config.get("completion_params", {}),
})
inst = resolve_model_instance(tenant_id, None) # tenant default; read real provider/name off it
return ModelConfig.model_validate({
"provider": inst.provider, "name": inst.model_name, "mode": "chat", "completion_params": {},
})
def _model_dict(mc: ModelConfig) -> dict[str, Any]:
mode = mc.mode.value if hasattr(mc.mode, "value") else str(mc.mode)
return {
"provider": mc.provider,
"name": mc.name,
"mode": mode,
"completion_params": dict(mc.completion_params) or {"temperature": 0.7},
}
def build_nodes(tenant_id: str, model_config: dict[str, Any], plan_items: list[str]) -> list[MutationIntent]:
try:
mc = _generator_model_config(tenant_id, model_config)
result = WorkflowGeneratorService.generate_workflow_graph(
tenant_id=tenant_id,
mode="workflow",
instruction="\n".join(plan_items),
model_config=mc,
current_graph=None,
)
graph = result.get("graph") or {}
if result.get("error") or not graph.get("nodes"):
return []
intents = graph_translate.to_intents(graph)
_ground(intents, mc, tenant_id, plan_items)
applicable, _rejected = graph_ops.filter_applicable({"nodes": [], "edges": []}, intents, _ALLOWED_NODE_TYPES)
return applicable
except Exception: # any generation/translation failure -> honest empty build
return []
def _ground(intents: list[MutationIntent], mc: ModelConfig, tenant_id: str, plan_items: list[str]) -> None:
model_dict = _model_dict(mc)
datasets = resources.list_tenant_resources(tenant_id).datasets
matched = [d.id for d in datasets if any(d.label in item for item in plan_items)]
for intent in intents:
if intent.op != "create_node":
continue
config = intent.args.get("config") or {}
if intent.args.get("node_type") == "llm":
config["model"] = model_dict # ground the model (real, configured provider)
elif intent.args.get("node_type") == "knowledge-retrieval":
config["dataset_ids"] = matched # ground datasets (real ids matched by label)
intent.args["config"] = config
@@ -0,0 +1,72 @@
from unittest.mock import patch
from services.dify_builder.agent import build, resources
_GEN_GRAPH = {
"graph": {
"nodes": [
{"id": "s", "type": "custom", "data": {"type": "start", "title": "Start", "variables": []}},
{
"id": "llm1",
"type": "custom",
"data": {
"type": "llm",
"title": "LLM",
"model": {"provider": "wrong", "name": "wrong"},
"prompt_template": [{"role": "system", "text": "hi"}],
},
},
{"id": "kb1", "type": "custom", "data": {"type": "knowledge-retrieval", "title": "KB", "dataset_ids": []}},
{"id": "e", "type": "custom", "data": {"type": "end", "title": "End", "outputs": []}},
],
"edges": [
{"id": "e1", "source": "s", "target": "llm1"},
{"id": "e2", "source": "llm1", "target": "kb1"},
{"id": "e3", "source": "kb1", "target": "e"},
],
},
"error": "",
"errors": [],
}
def _fake_mc(provider, name):
from core.app.app_config.entities import ModelConfig
return ModelConfig.model_validate({"provider": provider, "name": name, "mode": "chat", "completion_params": {}})
def test_build_nodes_grounds_model_and_dataset():
class _Inst:
provider, model_name = "anthropic", "claude-opus-4-8"
fake_resources = resources.TenantResources(
models=[], datasets=[resources.ResourceRef(id="kb-real", label="Company KB")], tools=[]
)
with (
patch.object(build, "_generator_model_config", return_value=_fake_mc("anthropic", "claude-opus-4-8")),
patch(
"services.dify_builder.agent.build.WorkflowGeneratorService.generate_workflow_graph",
return_value=_GEN_GRAPH,
),
patch.object(build.resources, "list_tenant_resources", return_value=fake_resources),
):
intents = build.build_nodes("t1", {}, ["Retrieve from Company KB", "Summarize"])
by_type = {}
for intent in intents:
if intent.op == "create_node":
by_type[intent.args["node_type"]] = intent.args["config"]
assert by_type["llm"]["model"]["provider"] == "anthropic" # ungrounded model overwritten
assert by_type["knowledge-retrieval"]["dataset_ids"] == ["kb-real"] # dataset injected by label match
def test_build_nodes_degrades_to_empty_on_generator_error():
with (
patch.object(build, "_generator_model_config", return_value=_fake_mc("anthropic", "x")),
patch(
"services.dify_builder.agent.build.WorkflowGeneratorService.generate_workflow_graph",
return_value={"graph": {"nodes": [], "edges": []}, "error": "boom", "errors": []},
),
):
assert build.build_nodes("t1", {}, ["x"]) == []