mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
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:
1 parent
6f27a709ba
commit
ba7692e2e9
4 files changed
+85
-4
No files matched your search
@@ -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 []
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user