11import logging
2+ from typing import Any
23
4+ from core .sandbox .entities .config import AppAssets
35from core .skill .entities .api_entities import NodeSkillInfo
4- from core .skill .entities .skill_metadata import ToolReference
5- from core .skill .entities .tool_dependencies import ToolDependency
6+ from core .skill .entities .skill_document import SkillDocument
7+ from core .skill .entities .tool_dependencies import ToolDependencies , ToolDependency
8+ from core .skill .skill_compiler import SkillCompiler
9+ from core .skill .skill_manager import SkillManager
610from core .workflow .enums import NodeType
11+ from models .model import App
712from models .workflow import Workflow
13+ from services .app_asset_service import AppAssetService
814
915logger = logging .getLogger (__name__ )
1016
@@ -15,13 +21,15 @@ class SkillService:
1521 """
1622
1723 @staticmethod
18- def get_node_skill_info (workflow : Workflow , node_id : str ) -> NodeSkillInfo :
24+ def get_node_skill_info (app : App , workflow : Workflow , node_id : str , user_id : str ) -> NodeSkillInfo :
1925 """
2026 Get skill information for a specific node in a workflow.
2127
2228 Args:
29+ app: The app model
2330 workflow: The workflow containing the node
2431 node_id: The ID of the node to get skill info for
32+ user_id: The user ID for asset access
2533
2634 Returns:
2735 NodeSkillInfo containing tool dependencies for the node
@@ -34,20 +42,26 @@ def get_node_skill_info(workflow: Workflow, node_id: str) -> NodeSkillInfo:
3442 if node_type != NodeType .LLM .value :
3543 return NodeSkillInfo (node_id = node_id )
3644
37- tool_dependencies = SkillService ._extract_tool_dependencies (node_data )
45+ # Check if node has any skill prompts
46+ if not SkillService ._has_skill (node_data ):
47+ return NodeSkillInfo (node_id = node_id )
48+
49+ tool_dependencies = SkillService ._extract_tool_dependencies_with_compiler (app , node_data , user_id )
3850
3951 return NodeSkillInfo (
4052 node_id = node_id ,
4153 tool_dependencies = tool_dependencies ,
4254 )
4355
4456 @staticmethod
45- def get_workflow_skills (workflow : Workflow ) -> list [NodeSkillInfo ]:
57+ def get_workflow_skills (app : App , workflow : Workflow , user_id : str ) -> list [NodeSkillInfo ]:
4658 """
4759 Get skill information for all nodes in a workflow that have skill references.
4860
4961 Args:
62+ app: The app model
5063 workflow: The workflow to scan for skills
64+ user_id: The user ID for asset access
5165
5266 Returns:
5367 List of NodeSkillInfo for nodes that have skill references
@@ -56,10 +70,10 @@ def get_workflow_skills(workflow: Workflow) -> list[NodeSkillInfo]:
5670
5771 # Only scan LLM nodes since they're the only ones that support skills
5872 for node_id , node_data in workflow .walk_nodes (specific_node_type = NodeType .LLM ):
59- has_skill = SkillService ._has_skill (node_data )
73+ has_skill = SkillService ._has_skill (dict ( node_data ) )
6074
6175 if has_skill :
62- tool_dependencies = SkillService ._extract_tool_dependencies ( node_data )
76+ tool_dependencies = SkillService ._extract_tool_dependencies_with_compiler ( app , dict ( node_data ), user_id )
6377 result .append (
6478 NodeSkillInfo (
6579 node_id = node_id ,
@@ -70,7 +84,7 @@ def get_workflow_skills(workflow: Workflow) -> list[NodeSkillInfo]:
7084 return result
7185
7286 @staticmethod
73- def _has_skill (node_data : dict ) -> bool :
87+ def _has_skill (node_data : dict [ str , Any ] ) -> bool :
7488 """Check if node has any skill prompts."""
7589 prompt_template = node_data .get ("prompt_template" , [])
7690 if isinstance (prompt_template , list ):
@@ -80,29 +94,67 @@ def _has_skill(node_data: dict) -> bool:
8094 return False
8195
8296 @staticmethod
83- def _extract_tool_dependencies (node_data : dict ) -> list [ToolDependency ]:
84- """Extract deduplicated tool dependencies from node data."""
85- dependencies : dict [str , ToolDependency ] = {}
97+ def _extract_tool_dependencies_with_compiler (
98+ app : App , node_data : dict [str , Any ], user_id : str
99+ ) -> list [ToolDependency ]:
100+ """Extract tool dependencies using SkillCompiler.
101+
102+ This method loads the SkillBundle and AppAssetFileTree, then uses
103+ SkillCompiler.compile_one() to properly extract tool dependencies
104+ including transitive dependencies from referenced skill files.
105+ """
106+ # Get the draft assets to obtain assets_id and file_tree
107+ assets = AppAssetService .get_assets (
108+ tenant_id = app .tenant_id ,
109+ app_id = app .id ,
110+ user_id = user_id ,
111+ is_draft = True ,
112+ )
113+
114+ if not assets :
115+ logger .warning ("No draft assets found for app_id=%s" , app .id )
116+ return []
117+
118+ assets_id = assets .id
119+ file_tree = assets .asset_tree
120+
121+ # Load the skill bundle
122+ try :
123+ bundle = SkillManager .load_bundle (
124+ tenant_id = app .tenant_id ,
125+ app_id = app .id ,
126+ assets_id = assets_id ,
127+ )
128+ except Exception as e :
129+ logger .debug ("Failed to load skill bundle for app_id=%s: %s" , app .id , e )
130+ # Return empty if bundle doesn't exist (no skills compiled yet)
131+ return []
132+
133+ # Compile each skill prompt and collect tool dependencies
134+ compiler = SkillCompiler ()
135+ tool_deps_list : list [ToolDependencies ] = []
86136
87137 prompt_template = node_data .get ("prompt_template" , [])
88138 if isinstance (prompt_template , list ):
89139 for prompt in prompt_template :
90140 if isinstance (prompt , dict ) and prompt .get ("skill" , False ):
91- metadata_dict = prompt .get ("metadata" ) or {}
92- tools_dict = metadata_dict .get ("tools" , {})
93-
94- for uuid , tool_data in tools_dict .items ():
95- if isinstance (tool_data , dict ):
96- try :
97- ref = ToolReference .model_validate ({"uuid" : uuid , ** tool_data })
98- key = f"{ ref .provider } .{ ref .tool_name } "
99- if key not in dependencies :
100- dependencies [key ] = ToolDependency (
101- type = ref .type ,
102- provider = ref .provider ,
103- tool_name = ref .tool_name ,
104- )
105- except Exception :
106- logger .debug ("Skipping invalid tool reference: uuid=%s" , uuid )
107-
108- return list (dependencies .values ())
141+ text : str = prompt .get ("text" , "" )
142+ metadata : dict [str , Any ] = prompt .get ("metadata" ) or {}
143+
144+ skill_entry = compiler .compile_one (
145+ bundle = bundle ,
146+ document = SkillDocument (skill_id = "anonymous" , content = text , metadata = metadata ),
147+ file_tree = file_tree ,
148+ base_path = AppAssets .PATH ,
149+ )
150+ tool_deps_list .append (skill_entry .tools )
151+
152+ if not tool_deps_list :
153+ return []
154+
155+ # Merge all tool dependencies
156+ from functools import reduce
157+
158+ merged = reduce (lambda x , y : x .merge (y ), tool_deps_list )
159+
160+ return merged .dependencies
0 commit comments