Files
dify/api/services/dify_builder/agent/resources.py
T
Charles YaoandClaude Opus 4.8 a5f8d71eb7 fix(dify-builder): dataset visibility + normalization guard + test coverage
FINDING 1 (dataset visibility): DatasetService.get_datasets requires user
parameter and silently omits ONLY_ME/PARTIAL_TEAM datasets on RBAC tenants.
Query Dataset model directly by tenant_id for complete grounding inventory.

FINDING 2 (normalization guard): Move fetch+normalize inside _safe to prevent
malformed records from raising after _safe returns. Use lambda builders to wrap
complete resource transformation including record normalization.

FINDING 3 (test coverage): Add test_mixed_failure_partial_success (one source
fails, others succeed) and test_malformed_tool_record_degrades (missing
tool_name key gracefully degrades).

Minor: compute model id once and reuse for both id and label fields; ensure
E302 two blank lines between test functions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:24:56 -07:00

79 lines
2.4 KiB
Python

"""Read-only tenant resource inventory for Dify Builder cognition/grounding.
One entry point -- list_tenant_resources -- returns the tenant's configured
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.
"""
from dataclasses import dataclass
from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.workflow.generator.tool_catalogue import build_tool_catalogue
from extensions.ext_database import db
from models.dataset import Dataset
from services.model_provider_service import ModelProviderService
@dataclass(frozen=True)
class ResourceRef:
id: str
label: str
meta: str = ""
readiness: str = "ready"
@dataclass(frozen=True)
class TenantResources:
models: list[ResourceRef]
datasets: list[ResourceRef]
tools: list[ResourceRef]
def _list_models(tenant_id: str) -> list[Any]:
return ModelProviderService().get_models_by_model_type(tenant_id=tenant_id, model_type="llm")
def _list_datasets(tenant_id: str) -> list[Any]:
with Session(db.engine) as session:
return list(session.scalars(select(Dataset).where(Dataset.tenant_id == tenant_id)).all())
def _list_tools(tenant_id: str) -> list[dict]:
return list(build_tool_catalogue(tenant_id))
def _safe(build) -> list:
try:
return build()
except Exception: # a missing/mis-configured source degrades to empty, never raises
return []
def list_tenant_resources(tenant_id: str) -> TenantResources:
def build_models():
models: list[ResourceRef] = []
for prov in _list_models(tenant_id):
for m in getattr(prov, "models", []):
name = getattr(m, "model", "")
model_id = f"{prov.provider}/{name}"
models.append(ResourceRef(id=model_id, label=model_id))
return models
def build_datasets():
return [ResourceRef(id=str(d.id), label=str(d.name)) for d in _list_datasets(tenant_id)]
def build_tools():
return [
ResourceRef(id=f"{t['provider_name']}/{t['tool_name']}", label=str(t.get("tool_label") or t["tool_name"]),
meta=str(t.get("description") or ""))
for t in _list_tools(tenant_id)
]
models = _safe(build_models)
datasets = _safe(build_datasets)
tools = _safe(build_tools)
return TenantResources(models=models, datasets=datasets, tools=tools)