feat(dify-builder): log build_nodes empty/failed generation and starved resource sources

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Charles YaoandClaude Opus 4.8 committed 2026-08-30 10:03:48 -07:00
1 parent 6f27a709ba
commit ba7692e2e9
4 files changed
+85 -4

No files matched your search

+14
View File
@@ -5,6 +5,7 @@ Each degrades to an honest result on model-None / provider-error / parse-fail
rather than crashing the advance. build_nodes lives in the same module
(added in Task A6)."""
import logging
from typing import Any
from core.app.app_config.entities import ModelConfig
@@ -16,6 +17,8 @@ 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
logger = logging.getLogger(__name__)
def analyze_goal(model, goal_text: str) -> dict[str, Any]:
if model is None:
@@ -152,12 +155,23 @@ def build_nodes(tenant_id: str, model_config: dict[str, Any], plan_items: list[s
)
graph = result.get("graph") or {}
if result.get("error") or not graph.get("nodes"):
logger.warning(
"Dify Builder: build_nodes produced no graph for tenant %s (%d plan items): error=%s",
tenant_id,
len(plan_items),
result.get("error"),
)
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
logger.exception(
"Dify Builder: build_nodes generation failed for tenant %s (%d plan items); returning empty build",
tenant_id,
len(plan_items),
)
return []
+12 -4
View File
@@ -5,6 +5,7 @@ models, datasets, and tools normalized to id+label. Each source degrades to
an empty list on failure so a missing provider never breaks the advance.
"""
import logging
from dataclasses import dataclass
from typing import Any
@@ -16,6 +17,8 @@ from extensions.ext_database import db
from models.dataset import Dataset
from services.model_provider_service import ModelProviderService
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class ResourceRef:
@@ -45,10 +48,15 @@ def _list_tools(tenant_id: str) -> list[dict]:
return list(build_tool_catalogue(tenant_id))
def _safe(build) -> list:
def _safe(build, source: str) -> list:
try:
return build()
except Exception: # a missing/mis-configured source degrades to empty, never raises
logger.warning(
"Dify Builder: resource source %r failed for the current tenant; treating as empty",
source,
exc_info=True,
)
return []
@@ -72,7 +80,7 @@ def list_tenant_resources(tenant_id: str) -> TenantResources:
for t in _list_tools(tenant_id)
]
models = _safe(build_models)
datasets = _safe(build_datasets)
tools = _safe(build_tools)
models = _safe(build_models, "models")
datasets = _safe(build_datasets, "datasets")
tools = _safe(build_tools, "tools")
return TenantResources(models=models, datasets=datasets, tools=tools)
@@ -1,3 +1,4 @@
import logging
from unittest.mock import patch
from services.dify_builder.agent import build, resources
@@ -166,3 +167,35 @@ def test_build_nodes_dataset_ids_are_independent_lists_per_node():
assert kb_configs[0]["dataset_ids"] == ["kb-real"]
assert kb_configs[1]["dataset_ids"] == ["kb-real"]
assert kb_configs[0]["dataset_ids"] is not kb_configs[1]["dataset_ids"]
def test_build_nodes_logs_when_generator_reports_error(caplog):
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": {}, "error": "generator boom", "errors": []},
), \
caplog.at_level(logging.WARNING, logger="services.dify_builder.agent.build"):
out = build.build_nodes("t1", {}, ["do a thing"])
assert out == []
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
assert len(warnings) == 1
assert "no graph" in warnings[0].getMessage()
assert "generator boom" in warnings[0].getMessage()
def test_build_nodes_logs_traceback_when_generation_raises(caplog):
with patch.object(build, "_generator_model_config", return_value=_fake_mc("anthropic", "x")), \
patch(
"services.dify_builder.agent.build.WorkflowGeneratorService.generate_workflow_graph",
side_effect=RuntimeError("kaboom"),
), \
caplog.at_level(logging.ERROR, logger="services.dify_builder.agent.build"):
out = build.build_nodes("t1", {}, ["do a thing"])
assert out == []
errors = [r for r in caplog.records if r.levelno == logging.ERROR]
assert len(errors) == 1
assert "generation failed" in errors[0].getMessage()
assert errors[0].exc_info is not None
@@ -1,3 +1,4 @@
import logging
from unittest.mock import patch
@@ -71,3 +72,28 @@ def test_malformed_tool_record_degrades():
assert out.tools == []
assert out.models == []
assert out.datasets == []
def test_list_tenant_resources_logs_when_a_source_fails(caplog):
from services.dify_builder.agent import resources
class _Model:
def __init__(self, m):
self.model = m
class _ProvResp:
def __init__(self, p, ms):
self.provider, self.models = p, [_Model(m) for m in ms]
with patch.object(resources, "_list_models", return_value=[_ProvResp("anthropic", ["claude-opus-4-8"])]), \
patch.object(resources, "_list_datasets", side_effect=RuntimeError("db down")), \
patch.object(resources, "_list_tools", return_value=[]), \
caplog.at_level(logging.WARNING, logger="services.dify_builder.agent.resources"):
out = resources.list_tenant_resources("t1")
assert out.datasets == []
assert out.models[0].id == "anthropic/claude-opus-4-8"
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
assert len(warnings) == 1
assert "datasets" in warnings[0].getMessage()
assert warnings[0].exc_info is not None