Skip to content

Commit 9feedbc

Browse files
committed
feat: load skills from skill space
1 parent 8f9dc26 commit 9feedbc

3 files changed

Lines changed: 170 additions & 47 deletions

File tree

veadk/skills/skills_plugin.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import os
1718
from pathlib import Path
1819
from typing import Optional
1920

@@ -43,7 +44,7 @@ class SkillsPlugin(BasePlugin):
4344
# Without plugin (direct tool usage):
4445
agent = Agent(
4546
tools=[
46-
SkillsTool(skills_directory="./skills"),
47+
SkillsTool(skills_directory="./skills", skills_space_name="veadk-skillspace"),
4748
BashTool(skills_directory="./skills"),
4849
ReadFileTool(),
4950
WriteFileTool(),
@@ -54,18 +55,28 @@ class SkillsPlugin(BasePlugin):
5455
# With plugin (auto-registration for multi-agent apps):
5556
app = App(
5657
root_agent=agent,
57-
plugins=[SkillsPlugin(skills_directory="./skills")]
58+
plugins=[SkillsPlugin(skills_directory="./skills", skills_space_name="veadk-skillspace")]
5859
)
5960
"""
6061

61-
def __init__(self, skills_directory: str | Path, name: str = "skills_plugin"):
62+
def __init__(
63+
self,
64+
skills_directory: str | Path,
65+
skills_space_name: Optional[str] = None,
66+
name: str = "skills_plugin",
67+
):
6268
"""Initialize the skills plugin.
6369
6470
Args:
6571
skills_directory: Path to directory containing skill folders.
6672
name: Name of the plugin instance.
6773
"""
6874
super().__init__(name)
75+
self.skills_space_name = skills_space_name or os.getenv("SKILL_SPACE_NAME")
76+
if not self.skills_space_name:
77+
logger.warning(
78+
"skills_space_name not provided and SKILL_SPACE_NAME environment variable is not set"
79+
)
6980
self.skills_directory = Path(skills_directory)
7081

7182
async def before_agent_callback(
@@ -82,10 +93,12 @@ async def before_agent_callback(
8293
initialize_session_path(session_id, str(self.skills_directory))
8394
logger.debug(f"Initialized session path for session: {session_id}")
8495

85-
add_skills_tool_to_agent(self.skills_directory, agent)
96+
add_skills_tool_to_agent(self.skills_directory, self.skills_space_name, agent)
8697

8798

88-
def add_skills_tool_to_agent(skills_directory: str | Path, agent: BaseAgent) -> None:
99+
def add_skills_tool_to_agent(
100+
skills_directory: str | Path, skills_space_name: Optional[str], agent: BaseAgent
101+
) -> None:
89102
"""Utility function to add Skills and Bash tools to a given agent.
90103
91104
Args:
@@ -97,5 +110,5 @@ def add_skills_tool_to_agent(skills_directory: str | Path, agent: BaseAgent) ->
97110
return
98111

99112
skills_directory = Path(skills_directory)
100-
agent.tools.append(SkillsToolset(skills_directory))
113+
agent.tools.append(SkillsToolset(skills_directory, skills_space_name))
101114
logger.debug(f"Added skills toolset to agent: {agent.name}")

veadk/tools/skills_tools/skills_tool.py

Lines changed: 149 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414

1515
from __future__ import annotations
1616

17+
import json
1718
from pathlib import Path
18-
from typing import Any, Dict
19+
from typing import Any, Dict, Optional
1920

2021
import yaml
2122
from google.adk.tools import BaseTool, ToolContext
2223
from google.genai import types
24+
25+
from veadk.utils.volcengine_sign import ve_request
2326
from veadk.utils.logger import get_logger
2427

2528
logger = 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

veadk/tools/skills_tools/skills_toolset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class SkillsToolset(BaseToolset):
5656
Note: For file upload/download, use the ArtifactsToolset separately.
5757
"""
5858

59-
def __init__(self, skills_directory: str | Path):
59+
def __init__(self, skills_directory: str | Path, skills_space_name: Optional[str]):
6060
"""Initialize the skills toolset.
6161
6262
Args:
@@ -66,7 +66,7 @@ def __init__(self, skills_directory: str | Path):
6666
self.skills_directory = Path(skills_directory)
6767

6868
# Create skills tools
69-
self.skills_tool = SkillsTool(self.skills_directory)
69+
self.skills_tool = SkillsTool(self.skills_directory, skills_space_name)
7070
self.read_file_tool = FunctionTool(func=read_file_tool)
7171
self.write_file_tool = FunctionTool(write_file_tool)
7272
self.edit_file_tool = FunctionTool(edit_file_tool)

0 commit comments

Comments
 (0)