Skip to content

Commit e23f987

Browse files
committed
fix: restore downloading skill from tos in skills tool
1 parent 406924d commit e23f987

3 files changed

Lines changed: 221 additions & 88 deletions

File tree

veadk/agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ def load_skills(self):
356356
f"Unsupported skill mode {self.skills_mode}, use `skills_sandbox`, `aio_sandbox` or `local` instead."
357357
)
358358

359+
if self.skills_mode == "skills_sandbox":
360+
self.instruction += (
361+
"You can use the skills by calling the `execute_skills` tool.\n\n"
362+
)
363+
359364
self.tools.append(SkillsToolset(skills, self.skills_mode))
360365
else:
361366
logger.warning("No skills loaded.")

veadk/tools/builtin_tools/execute_skills.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,29 @@ def execute_skills(
126126

127127
cmd = ["python", "agent.py", workflow_prompt]
128128

129+
res = ve_request(
130+
request_body={},
131+
action="GetCallerIdentity",
132+
ak=ak,
133+
sk=sk,
134+
service="sts",
135+
version="2018-01-01",
136+
region=region,
137+
host="sts.volcengineapi.com",
138+
header=header,
139+
)
140+
try:
141+
account_id = res["Result"]["AccountId"]
142+
except KeyError as e:
143+
logger.error(f"Error occurred while getting account id: {e}, response is {res}")
144+
return res
145+
129146
skill_space_id = os.getenv("SKILL_SPACE_ID", "")
130147
if not skill_space_id:
131148
logger.warning("SKILL_SPACE_ID environment variable is not set")
132149

133150
env_vars = {
151+
"TOS_SKILLS_DIR": f"tos://agentkit-platform-{account_id}/skills/",
134152
"SKILL_SPACE_ID": skill_space_id,
135153
"TOOL_USER_SESSION_ID": tool_user_session_id,
136154
}

veadk/tools/skills_tools/skills_tool.py

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

1515
from __future__ import annotations
1616

17-
import os
1817
from pathlib import Path
1918
from typing import Any, Dict
2019

@@ -61,6 +60,7 @@ def _generate_description(self) -> str:
6160
' - command: "data-analysis" - invoke the data-analysis skill\n'
6261
' - command: "pdf-processing" - invoke the pdf-processing skill\n\n'
6362
"Important:\n"
63+
"- If the invoked skills are not in the available skills, this tool will automatically download these skills from the remote object storage bucket\n"
6464
"- Do not invoke a skill that is already loaded in the conversation\n"
6565
"- After loading a skill, use the bash tool for execution\n"
6666
"- If not specified, scripts are located in the skill-name/scripts subdirectory\n"
@@ -98,113 +98,223 @@ async def run_async(
9898

9999
def _invoke_skill(self, skill_name: str, tool_context: ToolContext) -> str:
100100
"""Load and return the full content of a skill."""
101-
if skill_name not in self.skills:
102-
return f"Error: Skill '{skill_name}' does not exist."
103101

104-
skill = self.skills[skill_name]
105102
working_dir = get_session_path(session_id=tool_context.session.id)
106103
skill_dir = working_dir / "skills"
107104

108-
if skill.skill_space_id:
109-
logger.info(f"Attempting to download skill '{skill_name}' from skill space")
110-
try:
111-
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
112-
from veadk.integrations.ve_tos.ve_tos import VeTOS
113-
114-
region = os.getenv("AGENTKIT_TOOL_REGION", "cn-beijing")
115-
116-
access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
117-
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
118-
session_token = ""
119-
120-
if not (access_key and secret_key):
121-
# Try to get from vefaas iam
122-
cred = get_credential_from_vefaas_iam()
123-
access_key = cred.access_key_id
124-
secret_key = cred.secret_access_key
125-
session_token = cred.session_token
126-
127-
tos_bucket, tos_path = skill.bucket_name, skill.path
128-
129-
# Initialize VeTOS client
130-
tos_client = VeTOS(
131-
ak=access_key,
132-
sk=secret_key,
133-
session_token=session_token,
134-
bucket_name=tos_bucket,
135-
region=region,
105+
if skill_name not in self.skills:
106+
# 1. Download skill from TOS if not found locally
107+
user_skill_dir = skill_dir / skill_name
108+
if not user_skill_dir.exists() or not user_skill_dir.is_dir():
109+
# Try to download from TOS
110+
logger.info(
111+
f"Skill '{skill_name}' not found locally or in skill space, attempting to download from TOS..."
136112
)
137113

138-
save_path = skill_dir / f"{skill_name}.zip"
114+
try:
115+
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
116+
from veadk.integrations.ve_tos.ve_tos import VeTOS
117+
import os
118+
119+
access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
120+
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
121+
session_token = ""
122+
123+
if not (access_key and secret_key):
124+
# Try to get from vefaas iam
125+
cred = get_credential_from_vefaas_iam()
126+
access_key = cred.access_key_id
127+
secret_key = cred.secret_access_key
128+
session_token = cred.session_token
129+
130+
tos_skills_dir = os.getenv(
131+
"TOS_SKILLS_DIR"
132+
) # e.g. tos://agentkit-skills/skills/
133+
134+
# Parse bucket and prefix from TOS_SKILLS_DIR
135+
if not tos_skills_dir:
136+
error_msg = (
137+
f"Error: TOS_SKILLS_DIR environment variable is not set. "
138+
f"Cannot download skill '{skill_name}' from TOS. "
139+
f"Please set TOS_SKILLS_DIR"
140+
)
141+
logger.error(error_msg)
142+
return error_msg
143+
144+
# Validate TOS_SKILLS_DIR format
145+
if not tos_skills_dir.startswith("tos://"):
146+
error_msg = (
147+
f"Error: TOS_SKILLS_DIR format is invalid: '{tos_skills_dir}'. "
148+
f"Expected format: tos://agentkit-platform-xxxxxx/skills/ "
149+
f"Cannot download skill '{skill_name}'."
150+
)
151+
logger.error(error_msg)
152+
return error_msg
153+
154+
# Parse bucket and prefix from TOS_SKILLS_DIR
155+
# Remove "tos://" prefix and split by first "/"
156+
path_without_protocol = tos_skills_dir[6:] # Remove "tos://"
157+
158+
if "/" not in path_without_protocol:
159+
# Only bucket name, no path
160+
tos_bucket = path_without_protocol.rstrip("/")
161+
tos_prefix = skill_name
162+
else:
163+
# Split bucket and path
164+
first_slash_idx = path_without_protocol.index("/")
165+
tos_bucket = path_without_protocol[:first_slash_idx]
166+
base_path = path_without_protocol[first_slash_idx + 1 :].rstrip(
167+
"/"
168+
)
139169

140-
success = tos_client.download(
141-
bucket_name=tos_bucket,
142-
object_key=tos_path,
143-
save_path=save_path,
144-
)
170+
# Combine base path with skill name
171+
if base_path:
172+
tos_prefix = f"{base_path}/{skill_name}"
173+
else:
174+
tos_prefix = skill_name
145175

146-
if not success:
147-
return f"Error: Failed to download skill '{skill_name}' from TOS."
176+
logger.info(
177+
f"Parsed TOS location - Bucket: {tos_bucket}, Prefix: {tos_prefix}"
178+
)
148179

149-
# Extract downloaded zip into the skill directory
150-
import zipfile
151-
import shutil
180+
# Initialize VeTOS client
181+
tos_client = VeTOS(
182+
ak=access_key,
183+
sk=secret_key,
184+
session_token=session_token,
185+
bucket_name=tos_bucket,
186+
)
152187

153-
# Remove existing skill directory to ensure clean extraction
154-
target_skill_dir = skill_dir / skill_name
155-
if target_skill_dir.exists():
156-
try:
157-
shutil.rmtree(target_skill_dir)
158-
logger.info(
159-
f"Removed existing skill directory: {target_skill_dir}"
160-
)
161-
except Exception as e:
162-
logger.warning(
163-
f"Failed to remove existing skill directory {target_skill_dir}: {e}"
164-
)
188+
# Download the skill directory from TOS
189+
success = tos_client.download_directory(
190+
bucket_name=tos_bucket,
191+
prefix=tos_prefix,
192+
local_dir=str(user_skill_dir),
193+
)
165194

166-
try:
167-
with zipfile.ZipFile(save_path, "r") as z:
168-
z.extractall(path=str(skill_dir))
169-
except zipfile.BadZipFile:
170-
logger.error(
171-
f"Downloaded file for '{skill_name}' is not a valid zip"
195+
if not success:
196+
return f"Error: Skill '{skill_name}' not found in TOS: {tos_bucket}/{tos_prefix}."
197+
198+
logger.info(
199+
f"Successfully downloaded skill '{skill_name}' from TOS: {tos_bucket}/{tos_prefix}."
172200
)
173-
return f"Error: Downloaded file for skill '{skill_name}' is not a valid zip archive."
201+
174202
except Exception as e:
175-
logger.error(f"Failed to extract skill zip for '{skill_name}': {e}")
176-
return (
177-
f"Error: Failed to extract skill '{skill_name}' from zip: {e}"
203+
logger.error(
204+
f"Failed to download skill '{skill_name}' from TOS: {e}"
178205
)
206+
return f"Error: Skill '{skill_name}' not found locally or in the skill space, and it failed to download from TOS: {e}."
207+
else:
208+
skill = self.skills[skill_name]
179209

210+
if skill.skill_space_id:
211+
# 2. Download skill from skill space if not found locally
180212
logger.info(
181-
f"Successfully downloaded skill '{skill_name}' from skill space"
182-
)
183-
184-
except Exception as e:
185-
logger.error(
186-
f"Failed to download skill '{skill_name}' from skill space: {e}"
213+
f"Attempting to download skill '{skill_name}' from skill space..."
187214
)
188-
return (
189-
f"Error: Skill '{skill_name}' not found locally and failed to download from skill space: {e}. "
190-
f"Check the available skills list in the tool description."
191-
)
192-
else:
193-
# Create symlink to skills directory
194-
skills_mount = Path(skill.path)
195-
skills_link = skill_dir / skill_name
196-
if skills_mount.exists() and not skills_link.exists():
197215
try:
198-
skills_link.symlink_to(skills_mount)
199-
logger.debug(f"Created symlink: {skills_link} -> {skills_mount}")
200-
except FileExistsError:
201-
# Symlink already exists (race condition from concurrent session setup)
202-
pass
216+
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
217+
from veadk.integrations.ve_tos.ve_tos import VeTOS
218+
219+
region = os.getenv("AGENTKIT_TOOL_REGION", "cn-beijing")
220+
221+
access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
222+
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
223+
session_token = ""
224+
225+
if not (access_key and secret_key):
226+
# Try to get from vefaas iam
227+
cred = get_credential_from_vefaas_iam()
228+
access_key = cred.access_key_id
229+
secret_key = cred.secret_access_key
230+
session_token = cred.session_token
231+
232+
tos_bucket, tos_path = skill.bucket_name, skill.path
233+
234+
# Initialize VeTOS client
235+
tos_client = VeTOS(
236+
ak=access_key,
237+
sk=secret_key,
238+
session_token=session_token,
239+
bucket_name=tos_bucket,
240+
region=region,
241+
)
242+
243+
save_path = skill_dir / f"{skill_name}.zip"
244+
245+
success = tos_client.download(
246+
bucket_name=tos_bucket,
247+
object_key=tos_path,
248+
save_path=save_path,
249+
)
250+
251+
if not success:
252+
return (
253+
f"Error: Failed to download skill '{skill_name}' from TOS."
254+
)
255+
256+
# Extract downloaded zip into the skill directory
257+
import zipfile
258+
import shutil
259+
260+
# Remove existing skill directory to ensure clean extraction
261+
target_skill_dir = skill_dir / skill_name
262+
if target_skill_dir.exists():
263+
try:
264+
shutil.rmtree(target_skill_dir)
265+
logger.info(
266+
f"Removed existing skill directory: {target_skill_dir}"
267+
)
268+
except Exception as e:
269+
logger.warning(
270+
f"Failed to remove existing skill directory {target_skill_dir}: {e}"
271+
)
272+
273+
try:
274+
with zipfile.ZipFile(save_path, "r") as z:
275+
z.extractall(path=str(skill_dir))
276+
except zipfile.BadZipFile:
277+
logger.error(
278+
f"Downloaded file for '{skill_name}' is not a valid zip"
279+
)
280+
return f"Error: Downloaded file for skill '{skill_name}' is not a valid zip archive."
281+
except Exception as e:
282+
logger.error(
283+
f"Failed to extract skill zip for '{skill_name}': {e}"
284+
)
285+
return f"Error: Failed to extract skill '{skill_name}' from zip: {e}"
286+
287+
logger.info(
288+
f"Successfully downloaded skill '{skill_name}' from skill space"
289+
)
290+
203291
except Exception as e:
204-
# Log but don't fail - skills can still be accessed via absolute path
205-
logger.warning(
206-
f"Failed to create skills symlink for {str(skills_mount)}: {e}"
292+
logger.error(
293+
f"Failed to download skill '{skill_name}' from skill space: {e}"
294+
)
295+
return (
296+
f"Error: Skill '{skill_name}' not found locally and failed to download from skill space: {e}. "
297+
f"Check the available skills list in the tool description."
207298
)
299+
else:
300+
# 3. Use the local skill
301+
# Create symlink to skills directory
302+
skills_mount = Path(skill.path)
303+
skills_link = skill_dir / skill_name
304+
if skills_mount.exists() and not skills_link.exists():
305+
try:
306+
skills_link.symlink_to(skills_mount)
307+
logger.debug(
308+
f"Created symlink: {skills_link} -> {skills_mount}"
309+
)
310+
except FileExistsError:
311+
# Symlink already exists (race condition from concurrent session setup)
312+
pass
313+
except Exception as e:
314+
# Log but don't fail - skills can still be accessed via absolute path
315+
logger.warning(
316+
f"Failed to create skills symlink for {str(skills_mount)}: {e}"
317+
)
208318

209319
skill_file = skill_dir / skill_name / "SKILL.md"
210320
if not skill_file.exists():

0 commit comments

Comments
 (0)