Files
dify/api/services/skill_service.py

161 lines
5.7 KiB
Python

import logging
from typing import Any
from core.sandbox.entities.config import AppAssets
from core.skill.entities.api_entities import NodeSkillInfo
from core.skill.entities.skill_document import SkillDocument
from core.skill.entities.tool_dependencies import ToolDependencies, ToolDependency
from core.skill.skill_compiler import SkillCompiler
from core.skill.skill_manager import SkillManager
from core.workflow.enums import NodeType
from models.model import App
from models.workflow import Workflow
from services.app_asset_service import AppAssetService
logger = logging.getLogger(__name__)
class SkillService:
"""
Service for managing and retrieving skill information from workflows.
"""
@staticmethod
def get_node_skill_info(app: App, workflow: Workflow, node_id: str, user_id: str) -> NodeSkillInfo:
"""
Get skill information for a specific node in a workflow.
Args:
app: The app model
workflow: The workflow containing the node
node_id: The ID of the node to get skill info for
user_id: The user ID for asset access
Returns:
NodeSkillInfo containing tool dependencies for the node
"""
node_config = workflow.get_node_config_by_id(node_id)
node_data = node_config.get("data", {})
node_type = node_data.get("type", "")
# Only LLM nodes support skills currently
if node_type != NodeType.LLM.value:
return NodeSkillInfo(node_id=node_id)
# Check if node has any skill prompts
if not SkillService._has_skill(node_data):
return NodeSkillInfo(node_id=node_id)
tool_dependencies = SkillService._extract_tool_dependencies_with_compiler(app, node_data, user_id)
return NodeSkillInfo(
node_id=node_id,
tool_dependencies=tool_dependencies,
)
@staticmethod
def get_workflow_skills(app: App, workflow: Workflow, user_id: str) -> list[NodeSkillInfo]:
"""
Get skill information for all nodes in a workflow that have skill references.
Args:
app: The app model
workflow: The workflow to scan for skills
user_id: The user ID for asset access
Returns:
List of NodeSkillInfo for nodes that have skill references
"""
result: list[NodeSkillInfo] = []
# Only scan LLM nodes since they're the only ones that support skills
for node_id, node_data in workflow.walk_nodes(specific_node_type=NodeType.LLM):
has_skill = SkillService._has_skill(dict(node_data))
if has_skill:
tool_dependencies = SkillService._extract_tool_dependencies_with_compiler(app, dict(node_data), user_id)
result.append(
NodeSkillInfo(
node_id=node_id,
tool_dependencies=tool_dependencies,
)
)
return result
@staticmethod
def _has_skill(node_data: dict[str, Any]) -> bool:
"""Check if node has any skill prompts."""
prompt_template = node_data.get("prompt_template", [])
if isinstance(prompt_template, list):
for prompt in prompt_template:
if isinstance(prompt, dict) and prompt.get("skill", False):
return True
return False
@staticmethod
def _extract_tool_dependencies_with_compiler(
app: App, node_data: dict[str, Any], user_id: str
) -> list[ToolDependency]:
"""Extract tool dependencies using SkillCompiler.
This method loads the SkillBundle and AppAssetFileTree, then uses
SkillCompiler.compile_one() to properly extract tool dependencies
including transitive dependencies from referenced skill files.
"""
# Get the draft assets to obtain assets_id and file_tree
assets = AppAssetService.get_assets(
tenant_id=app.tenant_id,
app_id=app.id,
user_id=user_id,
is_draft=True,
)
if not assets:
logger.warning("No draft assets found for app_id=%s", app.id)
return []
assets_id = assets.id
file_tree = assets.asset_tree
# Load the skill bundle
try:
bundle = SkillManager.load_bundle(
tenant_id=app.tenant_id,
app_id=app.id,
assets_id=assets_id,
)
except Exception as e:
logger.debug("Failed to load skill bundle for app_id=%s: %s", app.id, e)
# Return empty if bundle doesn't exist (no skills compiled yet)
return []
# Compile each skill prompt and collect tool dependencies
compiler = SkillCompiler()
tool_deps_list: list[ToolDependencies] = []
prompt_template = node_data.get("prompt_template", [])
if isinstance(prompt_template, list):
for prompt in prompt_template:
if isinstance(prompt, dict) and prompt.get("skill", False):
text: str = prompt.get("text", "")
metadata: dict[str, Any] = prompt.get("metadata") or {}
skill_entry = compiler.compile_one(
bundle=bundle,
document=SkillDocument(skill_id="anonymous", content=text, metadata=metadata),
file_tree=file_tree,
base_path=AppAssets.PATH,
)
tool_deps_list.append(skill_entry.tools)
if not tool_deps_list:
return []
# Merge all tool dependencies
from functools import reduce
merged = reduce(lambda x, y: x.merge(y), tool_deps_list)
return merged.dependencies