mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
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:
1 parent
0ba1f932a9
commit
dc85ea51a2
2 files changed
+142
-1
No files matched your search
@@ -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"]) == []
|
||||
Reference in new issue
Block a user