Skip to content

Commit f5f069e

Browse files
authored
feat: add download_skills_tool (#492)
1 parent bc3d54a commit f5f069e

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import json
18+
import os
19+
import zipfile
20+
from pathlib import Path
21+
from typing import Optional
22+
23+
24+
from veadk.integrations.ve_tos.ve_tos import VeTOS
25+
from veadk.utils.volcengine_sign import ve_request
26+
from veadk.utils.logger import get_logger
27+
28+
logger = get_logger(__name__)
29+
30+
31+
def download_skills_tool(
32+
skill_space_id: str, download_path: str, skill_names: Optional[list[str]] = None
33+
) -> str:
34+
"""
35+
Download skills from a skill space to local path.
36+
37+
Args:
38+
skill_space_id: The skill space ID to download from
39+
download_path: Local path to save downloaded skills
40+
skill_names: Optional list of specific skill names to download. If None, download all skills.
41+
42+
Returns:
43+
Success or error message
44+
"""
45+
try:
46+
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
47+
48+
# Get credentials
49+
access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
50+
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
51+
session_token = ""
52+
53+
if not (access_key and secret_key):
54+
cred = get_credential_from_vefaas_iam()
55+
access_key = cred.access_key_id
56+
secret_key = cred.secret_access_key
57+
session_token = cred.session_token
58+
59+
# Get service configuration
60+
service = os.getenv("AGENTKIT_TOOL_SERVICE_CODE", "agentkit")
61+
region = os.getenv("AGENTKIT_TOOL_REGION", "cn-beijing")
62+
host = os.getenv("AGENTKIT_SKILL_HOST", "open.volcengineapi.com")
63+
64+
# Call ListSkillsBySpaceId API
65+
request_body = {
66+
"SkillSpaceId": skill_space_id,
67+
"InnerTags": {"source": "sandbox"},
68+
}
69+
logger.info(f"ListSkillsBySpaceId request body: {request_body}")
70+
71+
response = ve_request(
72+
request_body=request_body,
73+
action="ListSkillsBySpaceId",
74+
ak=access_key,
75+
sk=secret_key,
76+
service=service,
77+
version="2025-10-30",
78+
region=region,
79+
host=host,
80+
header={"X-Security-Token": session_token},
81+
)
82+
83+
if isinstance(response, str):
84+
response = json.loads(response)
85+
86+
list_skills_result = response.get("Result")
87+
items = list_skills_result.get("Items", [])
88+
89+
if not items:
90+
return f"No skills found in skill space: {skill_space_id}"
91+
92+
# Filter skills if skill_names is provided
93+
skills_to_download = []
94+
for item in items:
95+
if not isinstance(item, dict):
96+
continue
97+
98+
skill_name = item.get("Name")
99+
tos_bucket = item.get("BucketName")
100+
tos_path = item.get("TosPath")
101+
102+
if not skill_name or not tos_bucket or not tos_path:
103+
continue
104+
105+
# If skill_names specified, only include matching skills
106+
if skill_names is None or skill_name in skill_names:
107+
skills_to_download.append(
108+
{"name": skill_name, "bucket": tos_bucket, "path": tos_path}
109+
)
110+
111+
if not skills_to_download:
112+
if skill_names:
113+
return f"No matching skills found for names: {skill_names}"
114+
return f"No valid skills found in skill space: {skill_space_id}"
115+
116+
# Ensure download path exists
117+
download_dir = Path(download_path)
118+
download_dir.mkdir(parents=True, exist_ok=True)
119+
120+
# Initialize VeTOS client
121+
tos_client = VeTOS(
122+
ak=access_key,
123+
sk=secret_key,
124+
session_token=session_token,
125+
region=region,
126+
)
127+
128+
# Download each skill
129+
downloaded_skills = []
130+
for skill in skills_to_download:
131+
skill_name = skill["name"]
132+
tos_bucket = skill["bucket"]
133+
tos_path = skill["path"]
134+
135+
logger.info(
136+
f"Downloading skill '{skill_name}' from tos://{tos_bucket}/{tos_path}"
137+
)
138+
139+
# Download zip file
140+
zip_path = download_dir / f"{skill_name}.zip"
141+
success = tos_client.download(
142+
bucket_name=tos_bucket,
143+
object_key=tos_path,
144+
save_path=str(zip_path),
145+
)
146+
147+
if not success:
148+
logger.warning(f"Failed to download skill '{skill_name}'")
149+
continue
150+
151+
# Extract zip file
152+
skill_extract_dir = download_dir / skill_name
153+
try:
154+
# Remove existing directory if exists
155+
if skill_extract_dir.exists():
156+
import shutil
157+
158+
shutil.rmtree(skill_extract_dir)
159+
160+
with zipfile.ZipFile(zip_path, "r") as z:
161+
z.extractall(path=str(download_dir))
162+
163+
logger.info(
164+
f"Successfully extracted skill '{skill_name}' to {skill_extract_dir}"
165+
)
166+
downloaded_skills.append(skill_name)
167+
168+
except zipfile.BadZipFile:
169+
logger.error(f"Downloaded file for '{skill_name}' is not a valid zip")
170+
except Exception as e:
171+
logger.error(f"Failed to extract skill '{skill_name}': {e}")
172+
finally:
173+
# Delete zip file
174+
if zip_path.exists():
175+
zip_path.unlink()
176+
logger.debug(f"Deleted zip file: {zip_path}")
177+
178+
if downloaded_skills:
179+
return f"Successfully downloaded {len(downloaded_skills)} skill(s): {', '.join(downloaded_skills)} to {download_path}"
180+
else:
181+
return "Failed to download any skills"
182+
183+
except Exception as e:
184+
logger.error(f"Error when downloading skills: {e}")
185+
return f"Error when downloading skills: {str(e)}"

0 commit comments

Comments
 (0)