Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions veadk/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ def load_skills(self):
f"Unsupported skill mode {self.skills_mode}, use `skills_sandbox`, `aio_sandbox` or `local` instead."
)

if self.skills_mode == "skills_sandbox":
self.instruction += (
"You can use the skills by calling the `execute_skills` tool.\n\n"
)

self.tools.append(SkillsToolset(skills, self.skills_mode))
else:
logger.warning("No skills loaded.")
Expand Down
2 changes: 1 addition & 1 deletion veadk/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ async def run(
)
logger.info(f"Run config: {run_config}")

if self.agent.skills:
if hasattr(self.agent, "skills") and self.agent.skills:
from veadk.tools.skills_tools.session_path import initialize_session_path

initialize_session_path(session_id)
Expand Down
18 changes: 18 additions & 0 deletions veadk/tools/builtin_tools/execute_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,29 @@ def execute_skills(

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

res = ve_request(
request_body={},
action="GetCallerIdentity",
ak=ak,
sk=sk,
service="sts",
version="2018-01-01",
region=region,
host="sts.volcengineapi.com",
header=header,
)
try:
account_id = res["Result"]["AccountId"]
except KeyError as e:
logger.error(f"Error occurred while getting account id: {e}, response is {res}")
return res

skill_space_id = os.getenv("SKILL_SPACE_ID", "")
if not skill_space_id:
logger.warning("SKILL_SPACE_ID environment variable is not set")

env_vars = {
"TOS_SKILLS_DIR": f"tos://agentkit-platform-{account_id}/skills/",
"SKILL_SPACE_ID": skill_space_id,
"TOOL_USER_SESSION_ID": tool_user_session_id,
}
Expand Down
286 changes: 198 additions & 88 deletions veadk/tools/skills_tools/skills_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

from __future__ import annotations

import os
from pathlib import Path
from typing import Any, Dict

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

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

skill = self.skills[skill_name]
working_dir = get_session_path(session_id=tool_context.session.id)
skill_dir = working_dir / "skills"

if skill.skill_space_id:
logger.info(f"Attempting to download skill '{skill_name}' from skill space")
try:
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
from veadk.integrations.ve_tos.ve_tos import VeTOS

region = os.getenv("AGENTKIT_TOOL_REGION", "cn-beijing")

access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
session_token = ""

if not (access_key and secret_key):
# Try to get from vefaas iam
cred = get_credential_from_vefaas_iam()
access_key = cred.access_key_id
secret_key = cred.secret_access_key
session_token = cred.session_token

tos_bucket, tos_path = skill.bucket_name, skill.path

# Initialize VeTOS client
tos_client = VeTOS(
ak=access_key,
sk=secret_key,
session_token=session_token,
bucket_name=tos_bucket,
region=region,
if skill_name not in self.skills:
# 1. Download skill from TOS if not found locally
user_skill_dir = skill_dir / skill_name
if not user_skill_dir.exists() or not user_skill_dir.is_dir():
# Try to download from TOS
logger.info(
f"Skill '{skill_name}' not found locally or in skill space, attempting to download from TOS..."
)

save_path = skill_dir / f"{skill_name}.zip"
try:
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
from veadk.integrations.ve_tos.ve_tos import VeTOS
import os

access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
session_token = ""

if not (access_key and secret_key):
# Try to get from vefaas iam
cred = get_credential_from_vefaas_iam()
access_key = cred.access_key_id
secret_key = cred.secret_access_key
session_token = cred.session_token

tos_skills_dir = os.getenv(
"TOS_SKILLS_DIR"
) # e.g. tos://agentkit-skills/skills/

# Parse bucket and prefix from TOS_SKILLS_DIR
if not tos_skills_dir:
error_msg = (
f"Error: TOS_SKILLS_DIR environment variable is not set. "
f"Cannot download skill '{skill_name}' from TOS. "
f"Please set TOS_SKILLS_DIR"
)
logger.error(error_msg)
return error_msg

# Validate TOS_SKILLS_DIR format
if not tos_skills_dir.startswith("tos://"):
error_msg = (
f"Error: TOS_SKILLS_DIR format is invalid: '{tos_skills_dir}'. "
f"Expected format: tos://agentkit-platform-xxxxxx/skills/ "
f"Cannot download skill '{skill_name}'."
)
logger.error(error_msg)
return error_msg

# Parse bucket and prefix from TOS_SKILLS_DIR
# Remove "tos://" prefix and split by first "/"
path_without_protocol = tos_skills_dir[6:] # Remove "tos://"

if "/" not in path_without_protocol:
# Only bucket name, no path
tos_bucket = path_without_protocol.rstrip("/")
tos_prefix = skill_name
else:
# Split bucket and path
first_slash_idx = path_without_protocol.index("/")
tos_bucket = path_without_protocol[:first_slash_idx]
base_path = path_without_protocol[first_slash_idx + 1 :].rstrip(
"/"
)

success = tos_client.download(
bucket_name=tos_bucket,
object_key=tos_path,
save_path=save_path,
)
# Combine base path with skill name
if base_path:
tos_prefix = f"{base_path}/{skill_name}"
else:
tos_prefix = skill_name

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

# Extract downloaded zip into the skill directory
import zipfile
import shutil
# Initialize VeTOS client
tos_client = VeTOS(
ak=access_key,
sk=secret_key,
session_token=session_token,
bucket_name=tos_bucket,
)

# Remove existing skill directory to ensure clean extraction
target_skill_dir = skill_dir / skill_name
if target_skill_dir.exists():
try:
shutil.rmtree(target_skill_dir)
logger.info(
f"Removed existing skill directory: {target_skill_dir}"
)
except Exception as e:
logger.warning(
f"Failed to remove existing skill directory {target_skill_dir}: {e}"
)
# Download the skill directory from TOS
success = tos_client.download_directory(
bucket_name=tos_bucket,
prefix=tos_prefix,
local_dir=str(user_skill_dir),
)

try:
with zipfile.ZipFile(save_path, "r") as z:
z.extractall(path=str(skill_dir))
except zipfile.BadZipFile:
logger.error(
f"Downloaded file for '{skill_name}' is not a valid zip"
if not success:
return f"Error: Skill '{skill_name}' not found in TOS: {tos_bucket}/{tos_prefix}."

logger.info(
f"Successfully downloaded skill '{skill_name}' from TOS: {tos_bucket}/{tos_prefix}."
)
return f"Error: Downloaded file for skill '{skill_name}' is not a valid zip archive."

except Exception as e:
logger.error(f"Failed to extract skill zip for '{skill_name}': {e}")
return (
f"Error: Failed to extract skill '{skill_name}' from zip: {e}"
logger.error(
f"Failed to download skill '{skill_name}' from TOS: {e}"
)
return f"Error: Skill '{skill_name}' not found locally or in the skill space, and it failed to download from TOS: {e}."
else:
skill = self.skills[skill_name]

if skill.skill_space_id:
# 2. Download skill from skill space if not found locally
logger.info(
f"Successfully downloaded skill '{skill_name}' from skill space"
)

except Exception as e:
logger.error(
f"Failed to download skill '{skill_name}' from skill space: {e}"
f"Attempting to download skill '{skill_name}' from skill space..."
)
return (
f"Error: Skill '{skill_name}' not found locally and failed to download from skill space: {e}. "
f"Check the available skills list in the tool description."
)
else:
# Create symlink to skills directory
skills_mount = Path(skill.path)
skills_link = skill_dir / skill_name
if skills_mount.exists() and not skills_link.exists():
try:
skills_link.symlink_to(skills_mount)
logger.debug(f"Created symlink: {skills_link} -> {skills_mount}")
except FileExistsError:
# Symlink already exists (race condition from concurrent session setup)
pass
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
from veadk.integrations.ve_tos.ve_tos import VeTOS

region = os.getenv("AGENTKIT_TOOL_REGION", "cn-beijing")

access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
session_token = ""

if not (access_key and secret_key):
# Try to get from vefaas iam
cred = get_credential_from_vefaas_iam()
access_key = cred.access_key_id
secret_key = cred.secret_access_key
session_token = cred.session_token

tos_bucket, tos_path = skill.bucket_name, skill.path

# Initialize VeTOS client
tos_client = VeTOS(
ak=access_key,
sk=secret_key,
session_token=session_token,
bucket_name=tos_bucket,
region=region,
)

save_path = skill_dir / f"{skill_name}.zip"

success = tos_client.download(
bucket_name=tos_bucket,
object_key=tos_path,
save_path=save_path,
)

if not success:
return (
f"Error: Failed to download skill '{skill_name}' from TOS."
)

# Extract downloaded zip into the skill directory
import zipfile
import shutil

# Remove existing skill directory to ensure clean extraction
target_skill_dir = skill_dir / skill_name
if target_skill_dir.exists():
try:
shutil.rmtree(target_skill_dir)
logger.info(
f"Removed existing skill directory: {target_skill_dir}"
)
except Exception as e:
logger.warning(
f"Failed to remove existing skill directory {target_skill_dir}: {e}"
)

try:
with zipfile.ZipFile(save_path, "r") as z:
z.extractall(path=str(skill_dir))
except zipfile.BadZipFile:
logger.error(
f"Downloaded file for '{skill_name}' is not a valid zip"
)
return f"Error: Downloaded file for skill '{skill_name}' is not a valid zip archive."
except Exception as e:
logger.error(
f"Failed to extract skill zip for '{skill_name}': {e}"
)
return f"Error: Failed to extract skill '{skill_name}' from zip: {e}"

logger.info(
f"Successfully downloaded skill '{skill_name}' from skill space"
)

except Exception as e:
# Log but don't fail - skills can still be accessed via absolute path
logger.warning(
f"Failed to create skills symlink for {str(skills_mount)}: {e}"
logger.error(
f"Failed to download skill '{skill_name}' from skill space: {e}"
)
return (
f"Error: Skill '{skill_name}' not found locally and failed to download from skill space: {e}. "
f"Check the available skills list in the tool description."
)
else:
# 3. Use the local skill
# Create symlink to skills directory
skills_mount = Path(skill.path)
skills_link = skill_dir / skill_name
if skills_mount.exists() and not skills_link.exists():
try:
skills_link.symlink_to(skills_mount)
logger.debug(
f"Created symlink: {skills_link} -> {skills_mount}"
)
except FileExistsError:
# Symlink already exists (race condition from concurrent session setup)
pass
except Exception as e:
# Log but don't fail - skills can still be accessed via absolute path
logger.warning(
f"Failed to create skills symlink for {str(skills_mount)}: {e}"
)

skill_file = skill_dir / skill_name / "SKILL.md"
if not skill_file.exists():
Expand Down