Files
dify/api/services/workflow_generator_service.py
crazywoola cad2af2de5 feat(workflow-generator): inject installed tool catalogue into planner+builder
The planner had no idea which tools the tenant actually has installed, so
``tool`` nodes were generated with hallucinated provider/tool names that
failed at draft sync. Add a tenant-scoped catalogue helper and pipe its
output into both planner and builder prompts.

- core/workflow/generator/tool_catalogue.py: enumerate hardcoded built-in
  providers plus plugin providers (via ToolManager.list_builtin_providers
  which already covers both), pull provider_name + tool_name + label +
  llm-facing description, cap to 80 entries to keep the prompt bounded,
  and render as a compact one-tool-per-line block. Per-provider failures
  are logged and skipped so one bad plugin can't kill generation.
- prompts/planner_prompts.py + prompts/builder_prompts.py: new optional
  catalogue section. Planner is told to pick concrete provider/tool
  identifiers from the list; builder is told to set provider_id /
  provider_name / tool_name to entries that actually exist.
- runner.py: thread the catalogue text through generate_workflow_graph
  → _run_planner / _run_builder. New param defaults to "" so unit tests
  and tool-less tenants still work unchanged.
- services/workflow_generator_service.py: build + format the catalogue
  for the calling tenant. Catalogue build failures (plugin daemon down,
  etc.) are logged and downgraded to "no catalogue" — never block
  generation outright.
- 5 new unit tests cover the catalogue formatter (empty/short/long
  descriptions, label dedupe, newline stripping); the existing 7 runner
  tests keep passing thanks to the default-empty param.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:47:24 +08:00

83 lines
3.1 KiB
Python

"""
Workflow generator service.
Thin facade over ``core.workflow.generator.WorkflowGenerator`` that owns the
model-manager / model-instance plumbing. Controllers call this; the pure
domain class never touches the model registry directly.
Pattern mirrors ``LLMGenerator.generate_rule_config`` — see
``core/llm_generator/llm_generator.py`` — but lives in ``services/`` because
the generator output is consumed at the application layer (sync_draft_workflow,
createApp) rather than from inside another workflow.
"""
import logging
from typing import Any
from core.app.app_config.entities import ModelConfig
from core.model_manager import ModelManager
from core.workflow.generator import WorkflowGenerator
from core.workflow.generator.tool_catalogue import build_tool_catalogue, format_tool_catalogue
from core.workflow.generator.types import WorkflowGenerateResultDict, WorkflowGenerationMode
from graphon.model_runtime.entities.model_entities import ModelType
logger = logging.getLogger(__name__)
class WorkflowGeneratorService:
"""
Coordinates model resolution with the workflow generator domain logic.
Single public method (``generate_workflow_graph``) keeps the surface area
minimal — the cmd+k `/create` flow is the only caller today.
"""
@classmethod
def generate_workflow_graph(
cls,
*,
tenant_id: str,
mode: WorkflowGenerationMode,
instruction: str,
model_config: ModelConfig,
ideal_output: str = "",
) -> WorkflowGenerateResultDict:
"""
Resolve a model instance for the tenant and run the generator.
Errors from the LLM call (auth, quota, invoke) propagate so the
controller can map them to existing HTTP error envelopes (same
envelope as ``/rule-generate``).
"""
model_manager = ModelManager.for_tenant(tenant_id=tenant_id)
model_instance = model_manager.get_model_instance(
tenant_id=tenant_id,
model_type=ModelType.LLM,
provider=model_config.provider,
model=model_config.name,
)
model_parameters: dict[str, Any] = dict(model_config.completion_params or {})
# Build the installed-tool catalogue for this tenant so the planner/
# builder can pick concrete tools instead of inventing names. A failure
# here (plugin daemon unreachable, etc.) must not block generation —
# log and fall back to the no-tool catalogue path.
try:
tool_catalogue_text = format_tool_catalogue(build_tool_catalogue(tenant_id))
except Exception:
logger.exception("Workflow generator: failed to build tool catalogue for tenant %s", tenant_id)
tool_catalogue_text = ""
return WorkflowGenerator.generate_workflow_graph(
model_instance=model_instance,
model_parameters=model_parameters,
provider=model_config.provider,
model_name=model_config.name,
model_mode=str(model_config.mode),
mode=mode,
instruction=instruction,
ideal_output=ideal_output,
tool_catalogue_text=tool_catalogue_text,
)