diff --git a/veadk/agent.py b/veadk/agent.py index e57fa312..ae20e949 100644 --- a/veadk/agent.py +++ b/veadk/agent.py @@ -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.") diff --git a/veadk/runner.py b/veadk/runner.py index 7c901978..6fefb6a5 100644 --- a/veadk/runner.py +++ b/veadk/runner.py @@ -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) diff --git a/veadk/tools/builtin_tools/execute_skills.py b/veadk/tools/builtin_tools/execute_skills.py index 9aea3d36..281c7330 100644 --- a/veadk/tools/builtin_tools/execute_skills.py +++ b/veadk/tools/builtin_tools/execute_skills.py @@ -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, } diff --git a/veadk/tools/skills_tools/skills_tool.py b/veadk/tools/skills_tools/skills_tool.py index 05bc4158..435b1642 100644 --- a/veadk/tools/skills_tools/skills_tool.py +++ b/veadk/tools/skills_tools/skills_tool.py @@ -14,7 +14,6 @@ from __future__ import annotations -import os from pathlib import Path from typing import Any, Dict @@ -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" @@ -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():