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