mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
The Builder recommended every installed tool as ready to use without ever checking whether its provider actually has credentials configured. A credentialed tool with no tool_builtin_providers row got bound into a plan and then failed at test time with an authorization error the user can't act on. tool_catalogue.py now carries provider_controller.need_credentials onto each ToolCatalogueEntry as needs_credentials. resources.py resolves which providers are actually authorized for the tenant (mirroring ToolTransformService.builtin_provider_to_user_provider's need_credentials / db_provider rule exactly, so the Builder and the console's tools page can never disagree) and sets ResourceRef.readiness to "missing_config" for a credentialed tool with no authorized provider row, "ready" otherwise. Datasets and models are unaffected. If the authorization query itself fails, readiness fails open to "ready" rather than mass-labeling every credentialed tool unauthorized on a transient error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
5.0 KiB
Python
135 lines
5.0 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.
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from core.tools.tool_manager import ToolManager
|
|
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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@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 _authorized_providers(tenant_id: str) -> set[str]:
|
|
"""Providers with a credential row, i.e. authorized for this tenant.
|
|
|
|
Mirrors ``ToolTransformService.builtin_provider_to_user_provider``
|
|
(api/services/tools/tools_transform_service.py) so the Builder and the
|
|
console's own tools page can never disagree about what is usable: a
|
|
provider is usable when it needs no credentials, or when a
|
|
``tool_builtin_providers`` row exists for it.
|
|
"""
|
|
return {str(p.provider) for p in ToolManager.list_default_builtin_providers(tenant_id)}
|
|
|
|
|
|
def _tool_readiness(entry: dict, authorized: set[str] | None) -> str:
|
|
""""ready" unless the tool needs credentials it doesn't have.
|
|
|
|
``authorized is None`` means the authorization lookup itself failed
|
|
(see the ``build_tools`` try/except below) -- distinct from a
|
|
successful lookup that simply found zero rows. We fail OPEN in that
|
|
case: reporting every credentialed tool "missing_config" on a
|
|
transient DB error would hide tools the tenant genuinely has
|
|
configured, which is a worse failure than occasionally recommending
|
|
one we can't currently confirm. A real credential gap still surfaces
|
|
the next time the lookup succeeds, and the actual authorization check
|
|
happens again at tool-execution time regardless of this label.
|
|
"""
|
|
if not entry.get("needs_credentials"):
|
|
return "ready"
|
|
if authorized is None:
|
|
return "ready"
|
|
return "ready" if entry["provider_name"] in authorized else "missing_config"
|
|
|
|
|
|
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 []
|
|
|
|
|
|
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():
|
|
tools = _list_tools(tenant_id)
|
|
# Only pay for the authorization query when some listed tool would
|
|
# actually be gated on it -- a tenant with no credentialed tools
|
|
# installed never needs to touch tool_builtin_providers.
|
|
authorized: set[str] | None = None
|
|
if any(t.get("needs_credentials") for t in tools):
|
|
try:
|
|
authorized = _authorized_providers(tenant_id)
|
|
except Exception: # the lookup degrades to "unknown", not "none authorized"
|
|
logger.warning(
|
|
"Dify Builder: resource source %r failed for the current tenant; treating as empty",
|
|
"tool authorization",
|
|
exc_info=True,
|
|
)
|
|
authorized = None
|
|
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 ""), readiness=_tool_readiness(t, authorized))
|
|
for t in tools
|
|
]
|
|
|
|
models = _safe(build_models, "models")
|
|
datasets = _safe(build_datasets, "datasets")
|
|
tools = _safe(build_tools, "tools")
|
|
return TenantResources(models=models, datasets=datasets, tools=tools)
|