1414
1515from __future__ import annotations
1616
17+ import json
1718from pathlib import Path
18- from typing import Any , Dict
19+ from typing import Any , Dict , Optional
1920
2021import yaml
2122from google .adk .tools import BaseTool , ToolContext
2223from google .genai import types
24+
25+ from veadk .utils .volcengine_sign import ve_request
2326from veadk .utils .logger import get_logger
2427
2528logger = get_logger (__name__ )
@@ -32,13 +35,15 @@ class SkillsTool(BaseTool):
3235 tool description. Agent invokes a skill by name to load its full instructions.
3336 """
3437
35- def __init__ (self , skills_directory : str | Path ):
38+ def __init__ (self , skills_directory : str | Path , skills_space_name : Optional [ str ] ):
3639 self .skills_directory = Path (skills_directory ).resolve ()
3740 if not self .skills_directory .exists ():
3841 raise ValueError (
3942 f"Skills directory does not exist: { self .skills_directory } "
4043 )
4144
45+ self .skills_space_name = skills_space_name
46+
4247 self ._skill_cache : Dict [str , str ] = {}
4348
4449 # Generate description with available skills embedded
@@ -78,33 +83,95 @@ def _generate_description_with_skills(self) -> str:
7883
7984 def _discover_skills (self ) -> str :
8085 """Discover available skills and format as XML."""
81- if not self .skills_directory .exists ():
82- return "<available_skills>\n <!-- No skills directory found -->\n </available_skills>\n "
8386
8487 skills_entries = []
85- for skill_dir in sorted (self .skills_directory .iterdir ()):
86- if not skill_dir .is_dir ():
87- continue
88-
89- skill_file = skill_dir / "SKILL.md"
90- if not skill_file .exists ():
91- continue
9288
89+ # Discover skills from local directory
90+ if self .skills_directory .exists ():
91+ for skill_dir in sorted (self .skills_directory .iterdir ()):
92+ if not skill_dir .is_dir ():
93+ continue
94+
95+ skill_file = skill_dir / "SKILL.md"
96+ if not skill_file .exists ():
97+ continue
98+
99+ try :
100+ metadata = self ._parse_skill_metadata (skill_file )
101+ if metadata :
102+ skill_xml = (
103+ "<skill>\n "
104+ f"<name>{ metadata ['name' ]} </name>\n "
105+ f"<description>{ metadata ['description' ]} </description>\n "
106+ "</skill>"
107+ )
108+ skills_entries .append (skill_xml )
109+ except Exception as e :
110+ logger .error (f"Failed to parse skill { skill_dir .name } : { e } " )
111+
112+ # Discover skills from remote skill space
113+ if self .skills_space_name :
93114 try :
94- metadata = self ._parse_skill_metadata (skill_file )
95- if metadata :
115+ from veadk .auth .veauth .utils import get_credential_from_vefaas_iam
116+ import os
117+
118+ service = os .getenv ("AGENTKIT_TOOL_SERVICE_CODE" , "agentkit" )
119+ region = os .getenv ("AGENTKIT_TOOL_REGION" , "cn-beijing" )
120+ host = os .getenv ("AGENTKIT_SKILL_HOST" , "" )
121+ if not host :
122+ raise RuntimeError (
123+ "AGENTKIT_SKILL_HOST is not set; please provide it via environment variables"
124+ )
125+
126+ access_key = os .getenv ("VOLCENGINE_ACCESS_KEY" )
127+ secret_key = os .getenv ("VOLCENGINE_SECRET_KEY" )
128+ session_token = ""
129+
130+ if not (access_key and secret_key ):
131+ # Try to get from vefaas iam
132+ cred = get_credential_from_vefaas_iam ()
133+ access_key = cred .access_key_id
134+ secret_key = cred .secret_access_key
135+ session_token = cred .session_token
136+
137+ response = ve_request (
138+ request_body = {"SkillSpaceName" : self .skills_space_name },
139+ action = "ListSkillsBySpaceName" ,
140+ ak = access_key ,
141+ sk = secret_key ,
142+ service = service ,
143+ version = "2025-10-30" ,
144+ region = region ,
145+ host = host ,
146+ header = {"X-Security-Token" : session_token },
147+ )
148+
149+ if isinstance (response , str ):
150+ response = json .loads (response )
151+
152+ result = response .get ("Result" )
153+ items = result .get ("Items" )
154+
155+ for item in items :
156+ if not isinstance (item , dict ):
157+ continue
158+ name = item .get ("Name" )
159+ description = item .get ("Description" )
160+ if not name :
161+ continue
96162 skill_xml = (
97163 "<skill>\n "
98- f"<name>{ metadata [ ' name' ] } </name>\n "
99- f"<description>{ metadata [ ' description' ] } </description>\n "
164+ f"<name>{ name } </name>\n "
165+ f"<description>{ description } </description>\n "
100166 "</skill>"
101167 )
102168 skills_entries .append (skill_xml )
169+
103170 except Exception as e :
104- logger .error (f"Failed to parse skill { skill_dir . name } : { e } " )
171+ logger .error (f"Failed to discover skill from skill space : { e } " )
105172
106173 if not skills_entries :
107- return "<available_skills>\n <!-- No skills found -->\n </available_skills>\n "
174+ return "<available_skills>\n <!-- No skills found in skills directory and skill space -->\n </available_skills>\n "
108175
109176 return (
110177 "<available_skills>\n "
@@ -147,17 +214,25 @@ def _invoke_skill(self, skill_name: str) -> str:
147214
148215 # Find skill directory
149216 skill_dir = self .skills_directory / skill_name
150- if not skill_dir .exists () or not skill_dir .is_dir ():
151- # Try to download from TOS
217+ if not skill_dir .exists () or not skill_dir .is_dir () and self . skills_space_name :
218+ # Try to download from skill space
152219 logger .info (
153- f"Skill '{ skill_name } ' not found locally, attempting to download from TOS "
220+ f"Skill '{ skill_name } ' not found locally, attempting to download from skill space "
154221 )
155222
156223 try :
157224 from veadk .auth .veauth .utils import get_credential_from_vefaas_iam
158225 from veadk .integrations .ve_tos .ve_tos import VeTOS
159226 import os
160227
228+ service = os .getenv ("AGENTKIT_TOOL_SERVICE_CODE" , "agentkit" )
229+ region = os .getenv ("AGENTKIT_TOOL_REGION" , "cn-beijing" )
230+ host = os .getenv ("AGENTKIT_SKILL_HOST" , "" )
231+ if not host :
232+ raise RuntimeError (
233+ "AGENTKIT_SKILL_HOST is not set; please provide it via environment variables"
234+ )
235+
161236 access_key = os .getenv ("VOLCENGINE_ACCESS_KEY" )
162237 secret_key = os .getenv ("VOLCENGINE_SECRET_KEY" )
163238 session_token = ""
@@ -169,6 +244,28 @@ def _invoke_skill(self, skill_name: str) -> str:
169244 secret_key = cred .secret_access_key
170245 session_token = cred .session_token
171246
247+ response = ve_request (
248+ request_body = {
249+ "SkillSpaceName" : self .skills_space_name ,
250+ "SkillName" : skill_name ,
251+ },
252+ action = "GetSkillInfo" ,
253+ ak = access_key ,
254+ sk = secret_key ,
255+ service = service ,
256+ version = "2025-10-30" ,
257+ region = region ,
258+ host = host ,
259+ header = {"X-Security-Token" : session_token },
260+ )
261+
262+ if isinstance (response , str ):
263+ response = json .loads (response )
264+
265+ result = response .get ("Result" )
266+
267+ tos_path = result ["TosPath" ]
268+
172269 tos_skills_dir = os .getenv (
173270 "TOS_SKILLS_DIR"
174271 ) # e.g. tos://agentkit-skills/skills/
@@ -193,28 +290,20 @@ def _invoke_skill(self, skill_name: str) -> str:
193290 logger .error (error_msg )
194291 return error_msg
195292
196- # Parse bucket and prefix from TOS_SKILLS_DIR
293+ # Parse bucket from TOS_SKILLS_DIR
197294 # Remove "tos://" prefix and split by first "/"
198295 path_without_protocol = tos_skills_dir [6 :] # Remove "tos://"
199296
200297 if "/" not in path_without_protocol :
201298 # Only bucket name, no path
202299 tos_bucket = path_without_protocol .rstrip ("/" )
203- tos_prefix = skill_name
204300 else :
205301 # Split bucket and path
206302 first_slash_idx = path_without_protocol .index ("/" )
207303 tos_bucket = path_without_protocol [:first_slash_idx ]
208- base_path = path_without_protocol [first_slash_idx + 1 :].rstrip ("/" )
209-
210- # Combine base path with skill name
211- if base_path :
212- tos_prefix = f"{ base_path } /{ skill_name } "
213- else :
214- tos_prefix = skill_name
215304
216305 logger .info (
217- f"Parsed TOS location - Bucket: { tos_bucket } , Prefix : { tos_prefix } "
306+ f"Parsed TOS location - Bucket: { tos_bucket } , Path : { tos_path } "
218307 )
219308
220309 # Initialize VeTOS client
@@ -225,25 +314,46 @@ def _invoke_skill(self, skill_name: str) -> str:
225314 bucket_name = tos_bucket ,
226315 )
227316
228- # Download the skill directory from TOS
229- success = tos_client .download_directory (
317+ save_path = f"/tmp/{ skill_name } .zip"
318+
319+ success = tos_client .download (
230320 bucket_name = tos_bucket ,
231- prefix = tos_prefix ,
232- local_dir = str ( skill_dir ) ,
321+ object_key = tos_path ,
322+ save_path = save_path ,
233323 )
234324
235325 if not success :
326+ return f"Error: Failed to download skill '{ skill_name } ' from TOS."
327+
328+ # Extract downloaded zip into the skill directory
329+ import zipfile
330+
331+ skill_dir .parent .mkdir (parents = True , exist_ok = True )
332+
333+ try :
334+ with zipfile .ZipFile (save_path , "r" ) as z :
335+ z .extractall (path = str (skill_dir .parent ))
336+ except Exception as e :
337+ logger .error (f"Failed to extract skill zip for '{ skill_name } ': { e } " )
236338 return (
237- f"Error: Skill '{ skill_name } ' not found locally or in TOS registry "
238- f"({ tos_bucket } /{ tos_prefix } ). Check the available skills list in the tool description."
339+ f"Error: Failed to extract skill '{ skill_name } ' from zip: { e } "
239340 )
341+ finally :
342+ try :
343+ os .remove (save_path )
344+ except Exception :
345+ pass
240346
241- logger .info (f"Successfully downloaded skill '{ skill_name } ' from TOS" )
347+ logger .info (
348+ f"Successfully downloaded skill '{ skill_name } ' from skill space"
349+ )
242350
243351 except Exception as e :
244- logger .error (f"Failed to download skill '{ skill_name } ' from TOS: { e } " )
352+ logger .error (
353+ f"Failed to download skill '{ skill_name } ' from skill space: { e } "
354+ )
245355 return (
246- f"Error: Skill '{ skill_name } ' not found locally and failed to download from TOS : { e } . "
356+ f"Error: Skill '{ skill_name } ' not found locally and failed to download from skill space : { e } . "
247357 f"Check the available skills list in the tool description."
248358 )
249359
0 commit comments