From 5a10beeb9b6360e68485b69ad2de1393853d63c9 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 21 May 2026 21:07:30 +0800 Subject: [PATCH] Add sandbox agent tool routing and runner --- config.yaml.full | 16 ++ docs/docs/tools/builtin.md | 13 +- tests/tools/builtin_tools/test_agentkit.py | 164 ++++++++++++ .../langchain_ai/tools/execute_skills.py | 207 +++------------ .../community/langchain_ai/tools/run_code.py | 69 +---- veadk/tools/builtin_tools/_agentkit.py | 155 +++++++++++ veadk/tools/builtin_tools/coding.py | 33 +++ veadk/tools/builtin_tools/execute_skills.py | 248 ++---------------- veadk/tools/builtin_tools/run_code.py | 84 ++---- .../tools/builtin_tools/run_sandbox_agent.py | 203 ++++++++++++++ 10 files changed, 663 insertions(+), 529 deletions(-) create mode 100644 tests/tools/builtin_tools/test_agentkit.py create mode 100644 veadk/tools/builtin_tools/_agentkit.py create mode 100644 veadk/tools/builtin_tools/coding.py create mode 100644 veadk/tools/builtin_tools/run_sandbox_agent.py diff --git a/config.yaml.full b/config.yaml.full index af7650ee..89fcf664 100644 --- a/config.yaml.full +++ b/config.yaml.full @@ -37,6 +37,22 @@ volcengine: access_key: secret_key: +agentkit: + # [optional] default AgentKit tool id fallback for all sandbox tools + tool_id: + # [optional] dedicated tool id for `run_code` + tool_id_script: + # [optional] dedicated tool id for `execute_skills` + tool_id_skills: + # [optional] dedicated tool id for `coding` + tool_id_opencode: + # [optional] AgentKit endpoint configs + tool_host: + tool_service_code: agentkit + tool_region: cn-beijing + tool_scheme: https + top_scheme: https + tool: # [optional] https://console.volcengine.com/ask-echo/my-agent vesearch: diff --git a/docs/docs/tools/builtin.md b/docs/docs/tools/builtin.md index b8350645..7792d171 100644 --- a/docs/docs/tools/builtin.md +++ b/docs/docs/tools/builtin.md @@ -67,6 +67,9 @@ VeADK 集成了以下火山引擎工具: | `image_edit` | [编辑图片](https://www.volcengine.com/docs/82379/1541523)(图生图)。 | `from veadk.tools.builtin_tools.image_edit import image_edit` | | `video_generate` | 根据文本描述[生成视频](https.www.volcengine.com/docs/82379/1520757)。 | `from veadk.tools.builtin_tools.video_generate import video_generate` | | `run_code` | 在 [AgentKit 沙箱](https://console.volcengine.com/agentkit-ppe/region:agentkit-ppe+cn-beijing/builtintools)中执行代码。 | `from veadk.tools.builtin_tools.run_code import run_code` | +| `execute_skills` | 在预制技能沙箱中远程执行 `agent.py` 工作流。 | `from veadk.tools.builtin_tools.execute_skills import execute_skills` | +| `coding` | 在预制 OpenCode 沙箱中执行代码生成工作流。 | `from veadk.tools.builtin_tools.coding import coding` | +| `run_sandbox_agent` | 指定任意 `tool_id` 在远端 AgentKit 沙箱中执行 `agent.py`。 | `from veadk.tools.builtin_tools.run_sandbox_agent import run_sandbox_agent` | | `lark` | 集成[飞书开放能力](https://open.larkoffice.com/document/uAjLw4CM/ukTMukTMukTM/mcp_integration/mcp_installation),实现文档处理、会话管理等。 | `from veadk.tools.builtin_tools.lark import lark` | | `las` | 基于[火山引擎 AI 多模态数据湖服务 LAS](https://www.volcengine.com/mcp-marketplace) 进行数据管理。 | `from veadk.tools.builtin_tools.las import las` | | `mobile_run` | 手机指令执行 | `from veadk.tools.builtin_tools.mobile_run import create_mobile_use_tool` | @@ -183,7 +186,10 @@ VeADK 集成了以下火山引擎工具: 以下是必须在环境变量里面的配置项: - - `AGENTKIT_TOOL_ID`:用于调用火山引擎AgentKit Tools的沙箱环境Id + - `AGENTKIT_TOOL_ID`:默认的 AgentKit 沙箱环境 Id,会作为所有沙箱工具的兜底配置 + - `AGENTKIT_TOOL_ID_SCRIPT`:`run_code` 专用沙箱环境 Id,未配置时回退到 `AGENTKIT_TOOL_ID` + - `AGENTKIT_TOOL_ID_SKILLS`:`execute_skills` 专用沙箱环境 Id,未配置时回退到 `AGENTKIT_TOOL_ID` + - `AGENTKIT_TOOL_ID_OPENCODE`:`coding` 专用沙箱环境 Id,未配置时回退到 `AGENTKIT_TOOL_ID` - `AGENTKIT_TOOL_HOST`:用于调用火山引擎AgentKit Tools的EndPoint - `AGENTKIT_TOOL_SERVICE_CODE`:用于调用AgentKit Tools的ServiceCode - `AGENTKIT_TOOL_SCHEME`:用于切换调用 AgentKit Tools 的协议,允许 `http`/`https`,默认 `https` @@ -204,6 +210,11 @@ VeADK 集成了以下火山引擎工具: name: doubao-seed-1-6-250615 api_base: https://ark.cn-beijing.volces.com/api/v3/ api_key: your-api-key-here + agentkit: + tool_id: your-default-tool-id + tool_id_script: your-script-tool-id + tool_id_skills: your-skills-tool-id + tool_id_opencode: your-opencode-tool-id volcengine: # [optional] for Viking DB and `web_search` tool access_key: you-access-key-here diff --git a/tests/tools/builtin_tools/test_agentkit.py b/tests/tools/builtin_tools/test_agentkit.py new file mode 100644 index 00000000..2360f217 --- /dev/null +++ b/tests/tools/builtin_tools/test_agentkit.py @@ -0,0 +1,164 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib.util +import os +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +def _load_agentkit_module(): + module_path = ( + Path(__file__).resolve().parents[3] + / "veadk" + / "tools" + / "builtin_tools" + / "_agentkit.py" + ) + + fake_veadk = types.ModuleType("veadk") + fake_veadk.__path__ = [] # type: ignore[attr-defined] + fake_auth = types.ModuleType("veadk.auth") + fake_auth.__path__ = [] # type: ignore[attr-defined] + fake_veauth = types.ModuleType("veadk.auth.veauth") + fake_veauth.__path__ = [] # type: ignore[attr-defined] + fake_veauth_utils = types.ModuleType("veadk.auth.veauth.utils") + fake_config = types.ModuleType("veadk.config") + fake_utils = types.ModuleType("veadk.utils") + fake_utils.__path__ = [] # type: ignore[attr-defined] + fake_logger = types.ModuleType("veadk.utils.logger") + fake_sign = types.ModuleType("veadk.utils.volcengine_sign") + + def fake_getenv(env_name, default_value="", allow_false_values=False): + value = os.getenv(env_name, default_value) + if allow_false_values: + return value + if value: + return value + raise ValueError( + f"The environment variable `{env_name}` not exists. Please set this in your environment variable or config.yaml." + ) + + class _FakeCredential: + access_key_id = "ak" + secret_access_key = "sk" + session_token = "token" + + class _FakeLogger: + def debug(self, *_args, **_kwargs): + return None + + def warning(self, *_args, **_kwargs): + return None + + def error(self, *_args, **_kwargs): + return None + + fake_veauth_utils.get_credential_from_vefaas_iam = lambda: _FakeCredential() + fake_config.getenv = fake_getenv + fake_logger.get_logger = lambda _name: _FakeLogger() + fake_sign.ve_request = lambda **_kwargs: {"Result": {"AccountId": "test-account"}} + + stub_modules = { + "veadk": fake_veadk, + "veadk.auth": fake_auth, + "veadk.auth.veauth": fake_veauth, + "veadk.auth.veauth.utils": fake_veauth_utils, + "veadk.config": fake_config, + "veadk.utils": fake_utils, + "veadk.utils.logger": fake_logger, + "veadk.utils.volcengine_sign": fake_sign, + } + + with patch.dict(sys.modules, stub_modules): + spec = importlib.util.spec_from_file_location( + "test_agentkit_module", module_path + ) + module = importlib.util.module_from_spec(spec) + assert spec is not None + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +class TestResolveAgentkitToolId(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.agentkit_module = _load_agentkit_module() + + def setUp(self): + self.env_patcher = patch.dict( + os.environ, + {}, + clear=False, + ) + self.env_patcher.start() + for env_name in [ + "AGENTKIT_TOOL_ID", + "AGENTKIT_TOOL_ID_SCRIPT", + "AGENTKIT_TOOL_ID_SKILLS", + "AGENTKIT_TOOL_ID_OPENCODE", + ]: + os.environ.pop(env_name, None) + + def tearDown(self): + self.env_patcher.stop() + + def test_resolve_prefers_script_tool_id(self): + os.environ["AGENTKIT_TOOL_ID_SCRIPT"] = "script-tool" + os.environ["AGENTKIT_TOOL_ID"] = "default-tool" + + tool_id = self.agentkit_module.resolve_agentkit_tool_id( + "AGENTKIT_TOOL_ID_SCRIPT" + ) + + self.assertEqual(tool_id, "script-tool") + + def test_resolve_prefers_skills_tool_id(self): + os.environ["AGENTKIT_TOOL_ID_SKILLS"] = "skills-tool" + os.environ["AGENTKIT_TOOL_ID"] = "default-tool" + + tool_id = self.agentkit_module.resolve_agentkit_tool_id( + "AGENTKIT_TOOL_ID_SKILLS" + ) + + self.assertEqual(tool_id, "skills-tool") + + def test_resolve_prefers_opencode_tool_id(self): + os.environ["AGENTKIT_TOOL_ID_OPENCODE"] = "opencode-tool" + os.environ["AGENTKIT_TOOL_ID"] = "default-tool" + + tool_id = self.agentkit_module.resolve_agentkit_tool_id( + "AGENTKIT_TOOL_ID_OPENCODE" + ) + + self.assertEqual(tool_id, "opencode-tool") + + def test_resolve_falls_back_to_default_tool_id(self): + os.environ["AGENTKIT_TOOL_ID"] = "default-tool" + + tool_id = self.agentkit_module.resolve_agentkit_tool_id() + + self.assertEqual(tool_id, "default-tool") + + def test_resolve_raises_when_all_tool_ids_missing(self): + with self.assertRaisesRegex(ValueError, "AGENTKIT_TOOL_ID"): + self.agentkit_module.resolve_agentkit_tool_id("AGENTKIT_TOOL_ID_SCRIPT") + + +if __name__ == "__main__": + unittest.main() diff --git a/veadk/community/langchain_ai/tools/execute_skills.py b/veadk/community/langchain_ai/tools/execute_skills.py index b5a1fedd..d172b463 100644 --- a/veadk/community/langchain_ai/tools/execute_skills.py +++ b/veadk/community/langchain_ai/tools/execute_skills.py @@ -12,61 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import os from typing import List, Optional from langchain.tools import ToolRuntime, tool -from veadk.auth.veauth.utils import get_credential_from_vefaas_iam -from veadk.config import getenv +from veadk.tools.builtin_tools._agentkit import ( + get_agentkit_account_id, + resolve_agentkit_tool_id, +) +from veadk.tools.builtin_tools.run_sandbox_agent import ( + _format_execution_result, + _build_agent_command, + _build_agent_runner_code, +) from veadk.utils.logger import get_logger -from veadk.utils.volcengine_sign import ve_request +from veadk.tools.builtin_tools._agentkit import ( + get_agentkit_endpoint_config, + invoke_agentkit_run_code, +) logger = get_logger(__name__) -def _clean_ansi_codes(text: str) -> str: - """Remove ANSI escape sequences (color codes, etc.)""" - import re - - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - return ansi_escape.sub("", text) - - -def _format_execution_result(result_str: str) -> str: - """Format the execution results, handle escape characters and JSON structures""" - try: - result_json = json.loads(result_str) - - if not result_json.get("success"): - message = result_json.get("message", "Unknown error") - outputs = result_json.get("data", {}).get("outputs", []) - if outputs and isinstance(outputs[0], dict): - error_msg = outputs[0].get("ename", "Unknown error") - return f"Execution failed: {message}, {error_msg}" - - outputs = result_json.get("data", {}).get("outputs", []) - if not outputs: - return "No output generated" - - formatted_lines = [] - for output in outputs: - if output and isinstance(output, dict) and "text" in output: - text = output["text"] - text = _clean_ansi_codes(text) - text = text.replace("\\n", "\n") - formatted_lines.append(text) - - return "".join(formatted_lines).strip() - - except json.JSONDecodeError: - return _clean_ansi_codes(result_str) - except Exception as e: - logger.warning(f"Error formatting result: {e}, returning raw result") - return result_str - - @tool def execute_skills( workflow_prompt: str, @@ -86,15 +53,8 @@ def execute_skills( str: The output of the code execution. """ - tool_id = getenv("AGENTKIT_TOOL_ID") - - service = getenv( - "AGENTKIT_TOOL_SERVICE_CODE", "agentkit" - ) # temporary service for code run tool - region = getenv("AGENTKIT_TOOL_REGION", "cn-beijing") - host = getenv( - "AGENTKIT_TOOL_HOST", service + "." + region + ".volces.com" - ) # temporary host for code run tool + tool_id = resolve_agentkit_tool_id("AGENTKIT_TOOL_ID_SKILLS") + service, region, host, _ = get_agentkit_endpoint_config() logger.debug(f"tools endpoint: {host}") session_id = runtime.session_id # type: ignore @@ -107,130 +67,29 @@ def execute_skills( f"Execute skills in session_id={session_id}, tool_id={tool_id}, host={host}, service={service}, region={region}, timeout={timeout}" ) - header = {} - - ak = os.getenv("VOLCENGINE_ACCESS_KEY") - sk = os.getenv("VOLCENGINE_SECRET_KEY") - if not (ak and sk): - logger.debug( - "Get AK/SK from environment variables failed. Try to use credential from Iam." - ) - credential = get_credential_from_vefaas_iam() - ak = credential.access_key_id - sk = credential.secret_access_key - header = {"X-Security-Token": credential.session_token} - else: - logger.debug("Successfully get AK/SK from environment variables.") - - cmd = ["python", "agent.py", workflow_prompt] - if skills: - cmd.extend(["--skills"] + skills) - - # TODO: remove after agentkit supports custom environment variables setting - 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, - ) + cmd = _build_agent_command(workflow_prompt=workflow_prompt, skills=skills) try: - account_id = res["Result"]["AccountId"] + account_id = get_agentkit_account_id() except KeyError as e: - logger.error(f"Error occurred while getting account id: {e}, response is {res}") - return res + logger.error(f"Error occurred while getting account id: {e}") + return {"error": str(e)} - env_vars = { - "TOS_SKILLS_DIR": f"tos://agentkit-platform-{account_id}/skills/", - "TOOL_USER_SESSION_ID": tool_user_session_id, - } - - code = f""" -import subprocess -import os -import time -import select -import sys - -env = os.environ.copy() -for key, value in {env_vars!r}.items(): - if key not in env: - env[key] = value - -process = subprocess.Popen( - {cmd!r}, - cwd='/home/gem/veadk_skills', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - env=env, - bufsize=1, - universal_newlines=True -) + env_vars = {"TOOL_USER_SESSION_ID": tool_user_session_id} + if account_id: + env_vars["TOS_SKILLS_DIR"] = f"tos://agentkit-platform-{account_id}/skills/" -start_time = time.time() -timeout = {timeout - 10} - -with open('/tmp/agent.log', 'w') as log_file: - while True: - if time.time() - start_time > timeout: - process.kill() - log_file.write('log_type=stderr request_id=x function_id=y revision_number=1 Process timeout\\n') - break - - reads = [process.stdout.fileno(), process.stderr.fileno()] - ret = select.select(reads, [], [], 1) - - for fd in ret[0]: - if fd == process.stdout.fileno(): - line = process.stdout.readline() - if line: - log_file.write(f'log_type=stdout request_id=x function_id=y revision_number=1 {{line}}') - log_file.flush() - if fd == process.stderr.fileno(): - line = process.stderr.readline() - if line: - log_file.write(f'log_type=stderr request_id=x function_id=y revision_number=1 {{line}}') - log_file.flush() - - if process.poll() is not None: - break - - for line in process.stdout: - log_file.write(f'log_type=stdout request_id=x function_id=y revision_number=1 {{line}}') - for line in process.stderr: - log_file.write(f'log_type=stderr request_id=x function_id=y revision_number=1 {{line}}') - -with open('/tmp/agent.log', 'r') as log_file: - output = log_file.read() - print(output) - """ + code = _build_agent_runner_code( + cmd=cmd, + timeout=timeout, + env_vars=env_vars, + ) - res = ve_request( - request_body={ - "ToolId": tool_id, - "UserSessionId": tool_user_session_id, - "OperationType": "RunCode", - "OperationPayload": json.dumps( - { - "code": code, - "timeout": timeout, - "kernel_name": "python3", - } - ), - }, - action="InvokeTool", - ak=ak, - sk=sk, - service=service, - version="2025-10-30", - region=region, - host=host, - header=header, + res = invoke_agentkit_run_code( + tool_id=tool_id, + tool_user_session_id=tool_user_session_id, + code=code, + timeout=timeout, + kernel_name="python3", ) logger.debug(f"Invoke run code response: {res}") diff --git a/veadk/community/langchain_ai/tools/run_code.py b/veadk/community/langchain_ai/tools/run_code.py index 14356ede..87b51614 100644 --- a/veadk/community/langchain_ai/tools/run_code.py +++ b/veadk/community/langchain_ai/tools/run_code.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import os - from langchain.tools import ToolRuntime, tool -from veadk.auth.veauth.utils import get_credential_from_vefaas_iam -from veadk.config import getenv +from veadk.tools.builtin_tools._agentkit import ( + get_agentkit_endpoint_config, + invoke_agentkit_run_code, + resolve_agentkit_tool_id, +) from veadk.utils.logger import get_logger -from veadk.utils.volcengine_sign import ve_request logger = get_logger(__name__) @@ -39,18 +38,8 @@ def run_code(code: str, language: str, runtime: ToolRuntime, timeout: int = 30) str: The output of the code execution. """ - tool_id = getenv("AGENTKIT_TOOL_ID") - - service = getenv( - "AGENTKIT_TOOL_SERVICE_CODE", "agentkit" - ) # temporary service for code run tool - region = getenv("AGENTKIT_TOOL_REGION", "cn-beijing") - host = getenv( - "AGENTKIT_TOOL_HOST", service + "." + region + ".volces.com" - ) # temporary host for code run tool - scheme = os.getenv("AGENTKIT_TOOL_SCHEME", "https").lower() - if scheme not in {"http", "https"}: - scheme = "https" + tool_id = resolve_agentkit_tool_id("AGENTKIT_TOOL_ID_SCRIPT") + service, region, host, _ = get_agentkit_endpoint_config() logger.debug(f"tools endpoint: {host}") session_id = runtime.context.session_id # type: ignore @@ -64,44 +53,12 @@ def run_code(code: str, language: str, runtime: ToolRuntime, timeout: int = 30) f"Running code in language: {language}, session_id={session_id}, code={code}, tool_id={tool_id}, host={host}, service={service}, region={region}, timeout={timeout}" ) - header = {} - - logger.debug("Get AK/SK from tool context failed.") - ak = os.getenv("VOLCENGINE_ACCESS_KEY") - sk = os.getenv("VOLCENGINE_SECRET_KEY") - if not (ak and sk): - logger.debug( - "Get AK/SK from environment variables failed. Try to use credential from Iam." - ) - credential = get_credential_from_vefaas_iam() - ak = credential.access_key_id - sk = credential.secret_access_key - header = {"X-Security-Token": credential.session_token} - else: - logger.debug("Successfully get AK/SK from environment variables.") - - res = ve_request( - request_body={ - "ToolId": tool_id, - "UserSessionId": tool_user_session_id, - "OperationType": "RunCode", - "OperationPayload": json.dumps( - { - "code": code, - "timeout": timeout, - "kernel_name": language, - } - ), - }, - action="InvokeTool", - ak=ak, - sk=sk, - service=service, - version="2025-10-30", - region=region, - host=host, - header=header, - scheme=scheme, # type: ignore + res = invoke_agentkit_run_code( + tool_id=tool_id, + tool_user_session_id=tool_user_session_id, + code=code, + timeout=timeout, + kernel_name=language, ) logger.debug(f"Invoke run code response: {res}") diff --git a/veadk/tools/builtin_tools/_agentkit.py b/veadk/tools/builtin_tools/_agentkit.py new file mode 100644 index 00000000..8ed39189 --- /dev/null +++ b/veadk/tools/builtin_tools/_agentkit.py @@ -0,0 +1,155 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from typing import Any, Optional + +from veadk.auth.veauth.utils import get_credential_from_vefaas_iam +from veadk.config import getenv +from veadk.utils.logger import get_logger +from veadk.utils.volcengine_sign import ve_request + +logger = get_logger(__name__) + + +def resolve_agentkit_tool_id(*preferred_env_names: str) -> str: + """Resolve the first configured AgentKit tool id with AGENTKIT_TOOL_ID fallback.""" + for env_name in [*preferred_env_names, "AGENTKIT_TOOL_ID"]: + tool_id = os.getenv(env_name) + if tool_id: + return tool_id + + return getenv("AGENTKIT_TOOL_ID") + + +def get_agentkit_endpoint_config( + host_env_name: str = "AGENTKIT_TOOL_HOST", +) -> tuple[str, str, str, str]: + """Return service, region, host and scheme for AgentKit tool invocation.""" + service = getenv("AGENTKIT_TOOL_SERVICE_CODE", "agentkit") + + cloud_provider = (os.getenv("CLOUD_PROVIDER") or "").lower() + if cloud_provider == "byteplus": + sld = "bytepluses" + default_region = "ap-southeast-1" + else: + sld = "volces" + default_region = "cn-beijing" + + region = getenv("AGENTKIT_TOOL_REGION", default_region) + host = getenv(host_env_name, service + "." + region + f".{sld}.com") + scheme = getenv("AGENTKIT_TOOL_SCHEME", "https", allow_false_values=True).lower() + if scheme not in {"http", "https"}: + scheme = "https" + + return service, region, host, scheme + + +def get_agentkit_credentials( + tool_state: Optional[dict[str, Any]] = None, +) -> tuple[str, str, dict[str, str]]: + """Resolve AgentKit invocation credentials from tool state, env, or IAM.""" + ak = tool_state.get("VOLCENGINE_ACCESS_KEY") if tool_state else None + sk = tool_state.get("VOLCENGINE_SECRET_KEY") if tool_state else None + header: dict[str, str] = {} + + if not (ak and sk): + logger.debug("Get AK/SK from tool context failed.") + ak = os.getenv("VOLCENGINE_ACCESS_KEY") + sk = os.getenv("VOLCENGINE_SECRET_KEY") + if not (ak and sk): + logger.debug( + "Get AK/SK from environment variables failed. Try to use credential from Iam." + ) + credential = get_credential_from_vefaas_iam() + ak = credential.access_key_id + sk = credential.secret_access_key + header = {"X-Security-Token": credential.session_token} + else: + logger.debug("Successfully get AK/SK from environment variables.") + else: + logger.debug("Successfully get AK/SK from tool context.") + + return ak, sk, header + + +def get_agentkit_account_id(tool_state: Optional[dict[str, Any]] = None) -> str: + """Get the current caller account id for remote skills sandbox setup.""" + cloud_provider = (os.getenv("CLOUD_PROVIDER") or "").lower() + if cloud_provider == "vestack": + return "" + + _, region, _, _ = get_agentkit_endpoint_config() + ak, sk, header = get_agentkit_credentials(tool_state) + host = ( + "open.byteplusapi.com" + if cloud_provider == "byteplus" + else "sts.volcengineapi.com" + ) + res = ve_request( + request_body={}, + action="GetCallerIdentity", + ak=ak, + sk=sk, + service="sts", + version="2018-01-01", + region=region, + host=host, + header=header, + ) + return res["Result"]["AccountId"] + + +def invoke_agentkit_run_code( + *, + tool_id: str, + tool_user_session_id: str, + code: str, + timeout: int, + kernel_name: str, + tool_state: Optional[dict[str, Any]] = None, + ttl: Optional[int] = None, +) -> dict[str, Any]: + """Invoke the AgentKit RunCode operation.""" + service, region, host, scheme = get_agentkit_endpoint_config() + ak, sk, header = get_agentkit_credentials(tool_state) + + request_body: dict[str, Any] = { + "ToolId": tool_id, + "UserSessionId": tool_user_session_id, + "OperationType": "RunCode", + "OperationPayload": json.dumps( + { + "code": code, + "timeout": timeout, + "kernel_name": kernel_name, + } + ), + } + if ttl is not None: + request_body["Ttl"] = ttl + + return ve_request( + request_body=request_body, + action="InvokeTool", + ak=ak, + sk=sk, + service=service, + version="2025-10-30", + region=region, + host=host, + header=header, + scheme=scheme, + ) diff --git a/veadk/tools/builtin_tools/coding.py b/veadk/tools/builtin_tools/coding.py new file mode 100644 index 00000000..582d4e0f --- /dev/null +++ b/veadk/tools/builtin_tools/coding.py @@ -0,0 +1,33 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.adk.tools import ToolContext + +from veadk.tools.builtin_tools._agentkit import resolve_agentkit_tool_id +from veadk.tools.builtin_tools.run_sandbox_agent import run_sandbox_agent + + +def coding( + workflow_prompt: str, + tool_context: ToolContext = None, + timeout: int = 900, +) -> str: + """Create code with the pre-configured OpenCode AgentKit sandbox.""" + tool_id = resolve_agentkit_tool_id("AGENTKIT_TOOL_ID_OPENCODE") + return run_sandbox_agent( + workflow_prompt=workflow_prompt, + tool_id=tool_id, + tool_context=tool_context, + timeout=timeout, + ) diff --git a/veadk/tools/builtin_tools/execute_skills.py b/veadk/tools/builtin_tools/execute_skills.py index f37fac04..648af64b 100644 --- a/veadk/tools/builtin_tools/execute_skills.py +++ b/veadk/tools/builtin_tools/execute_skills.py @@ -12,60 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import os - from google.adk.tools import ToolContext -from veadk.config import getenv +from veadk.tools.builtin_tools._agentkit import ( + get_agentkit_account_id, + resolve_agentkit_tool_id, +) +from veadk.tools.builtin_tools.run_sandbox_agent import run_sandbox_agent from veadk.utils.logger import get_logger -from veadk.utils.volcengine_sign import ve_request -from veadk.auth.veauth.utils import get_credential_from_vefaas_iam logger = get_logger(__name__) -def _clean_ansi_codes(text: str) -> str: - """Remove ANSI escape sequences (color codes, etc.)""" - import re - - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - return ansi_escape.sub("", text) - - -def _format_execution_result(result_str: str) -> str: - """Format the execution results, handle escape characters and JSON structures""" - try: - result_json = json.loads(result_str) - - if not result_json.get("success"): - message = result_json.get("message", "Unknown error") - outputs = result_json.get("data", {}).get("outputs", []) - if outputs and isinstance(outputs[0], dict): - error_msg = outputs[0].get("ename", "Unknown error") - return f"Execution failed: {message}, {error_msg}" - - outputs = result_json.get("data", {}).get("outputs", []) - if not outputs: - return "No output generated" - - formatted_lines = [] - for output in outputs: - if output and isinstance(output, dict) and "text" in output: - text = output["text"] - text = _clean_ansi_codes(text) - text = text.replace("\\n", "\n") - formatted_lines.append(text) - - return "".join(formatted_lines).strip() - - except json.JSONDecodeError: - return _clean_ansi_codes(result_str) - except Exception as e: - logger.warning(f"Error formatting result: {e}, returning raw result") - return result_str - - def execute_skills( workflow_prompt: str, tool_context: ToolContext = None, @@ -80,188 +38,20 @@ def execute_skills( Returns: str: The output of the code execution. """ - timeout = 900 # The timeout in seconds for the code execution, less than or equal to 900. Defaults to 900. Hard-coded to prevent the Agent from adjusting this parameter. - - tool_id = getenv("AGENTKIT_TOOL_ID") - - service = getenv( - "AGENTKIT_TOOL_SERVICE_CODE", "agentkit" - ) # temporary service for code run tool - - cloud_provider = (os.getenv("CLOUD_PROVIDER") or "").lower() - if cloud_provider == "byteplus": - sld = "bytepluses" - default_region = "ap-southeast-1" - else: - sld = "volces" - default_region = "cn-beijing" - - region = getenv("AGENTKIT_TOOL_REGION", default_region) - host = getenv( - "AGENTKIT_TOOL_HOST", service + "." + region + f".{sld}.com" - ) # temporary host for code run tool - logger.debug(f"tools endpoint: {host}") - - session_id = tool_context._invocation_context.session.id - agent_name = tool_context._invocation_context.agent.name - user_id = tool_context._invocation_context.user_id - tool_user_session_id = agent_name + "_" + user_id + "_" + session_id - logger.debug(f"tool_user_session_id: {tool_user_session_id}") - - scheme = getenv("AGENTKIT_TOOL_SCHEME", "https", allow_false_values=True).lower() - if scheme not in {"http", "https"}: - scheme = "https" - - logger.debug( - f"Execute skills in session_id={session_id}, tool_id={tool_id}, host={host}, service={service}, region={region}, timeout={timeout}" - ) - - ak = tool_context.state.get("VOLCENGINE_ACCESS_KEY") - sk = tool_context.state.get("VOLCENGINE_SECRET_KEY") - header = {} - - if not (ak and sk): - logger.debug("Get AK/SK from tool context failed.") - ak = os.getenv("VOLCENGINE_ACCESS_KEY") - sk = os.getenv("VOLCENGINE_SECRET_KEY") - if not (ak and sk): - logger.debug( - "Get AK/SK from environment variables failed. Try to use credential from Iam." - ) - credential = get_credential_from_vefaas_iam() - ak = credential.access_key_id - sk = credential.secret_access_key - header = {"X-Security-Token": credential.session_token} - else: - logger.debug("Successfully get AK/SK from environment variables.") - else: - logger.debug("Successfully get AK/SK from tool context.") - - cmd = ["python", "agent.py", workflow_prompt] - - account_id = "" - if cloud_provider != "vestack": - res = ve_request( - request_body={}, - action="GetCallerIdentity", - ak=ak, - sk=sk, - service="sts", - version="2018-01-01", - region=region, - host="sts.volcengineapi.com" - if cloud_provider != "byteplus" - else "open.byteplusapi.com", - header=header, + timeout = 900 + tool_id = resolve_agentkit_tool_id("AGENTKIT_TOOL_ID_SKILLS") + account_id = get_agentkit_account_id(tool_context.state if tool_context else None) + + extra_env_vars = {} + if account_id: + extra_env_vars["TOS_SKILLS_DIR"] = ( + f"tos://agentkit-platform-{account_id}/skills/" ) - 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, - "PYTHONPATH": "$SRV_PYTHONPATH:$PYTHONPATH", - } - code = f""" -import subprocess -import os -import time -import select -import sys - -env = os.environ.copy() -for key, value in {env_vars!r}.items(): - if key not in env: - env[key] = value - -process = subprocess.Popen( - {cmd!r}, - cwd='/home/gem/veadk_skills', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - env=env, - bufsize=1, - universal_newlines=True -) - -start_time = time.time() -timeout = {timeout - 10} - -with open('/tmp/agent.log', 'w') as log_file: - while True: - if time.time() - start_time > timeout: - process.kill() - log_file.write('log_type=stderr request_id=x function_id=y revision_number=1 Process timeout\\n') - print("Process timeout", end='', file=sys.stderr) - break - - reads = [process.stdout.fileno(), process.stderr.fileno()] - ret = select.select(reads, [], [], 1) - - for fd in ret[0]: - if fd == process.stdout.fileno(): - line = process.stdout.readline() - if line: - log_file.write(f'log_type=stdout request_id=x function_id=y revision_number=1 {{line}}') - log_file.flush() - print(line, end='') - if fd == process.stderr.fileno(): - line = process.stderr.readline() - if line: - log_file.write(f'log_type=stderr request_id=x function_id=y revision_number=1 {{line}}') - log_file.flush() - print(line, end='', file=sys.stderr) - - if process.poll() is not None: - break - - for line in process.stdout: - log_file.write(f'log_type=stdout request_id=x function_id=y revision_number=1 {{line}}') - print(line, end='') - for line in process.stderr: - log_file.write(f'log_type=stderr request_id=x function_id=y revision_number=1 {{line}}') - print(line, end='', file=sys.stderr) - """ - - res = ve_request( - request_body={ - "ToolId": tool_id, - "UserSessionId": tool_user_session_id, - "OperationType": "RunCode", - "OperationPayload": json.dumps( - { - "code": code, - "timeout": timeout, - "kernel_name": "python3", - } - ), - }, - action="InvokeTool", - ak=ak, - sk=sk, - service=service, - version="2025-10-30", - region=region, - host=host, - header=header, - scheme=scheme, + return run_sandbox_agent( + workflow_prompt=workflow_prompt, + tool_id=tool_id, + tool_context=tool_context, + timeout=timeout, + extra_env_vars=extra_env_vars, ) - logger.debug(f"Invoke run code response: {res}") - - try: - return _format_execution_result(res["Result"]["Result"]) - except KeyError as e: - logger.error(f"Error occurred while running code: {e}, response is {res}") - return res diff --git a/veadk/tools/builtin_tools/run_code.py b/veadk/tools/builtin_tools/run_code.py index 22531ced..2f356a98 100644 --- a/veadk/tools/builtin_tools/run_code.py +++ b/veadk/tools/builtin_tools/run_code.py @@ -12,15 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import os from google.adk.tools import ToolContext -from veadk.auth.veauth.utils import get_credential_from_vefaas_iam -from veadk.config import getenv +from veadk.tools.builtin_tools._agentkit import ( + get_agentkit_endpoint_config, + invoke_agentkit_run_code, + resolve_agentkit_tool_id, +) from veadk.utils.logger import get_logger -from veadk.utils.volcengine_sign import ve_request logger = get_logger(__name__) @@ -40,27 +41,8 @@ def run_code( str: The output of the code execution. """ - tool_id = getenv("AGENTKIT_TOOL_ID") - - service = getenv( - "AGENTKIT_TOOL_SERVICE_CODE", "agentkit" - ) # temporary service for code run tool - - cloud_provider = (os.getenv("CLOUD_PROVIDER") or "").lower() - if cloud_provider == "byteplus": - sld = "bytepluses" - default_region = "ap-southeast-1" - else: - sld = "volces" - default_region = "cn-beijing" - - region = getenv("AGENTKIT_TOOL_REGION", default_region) - host = getenv( - "AGENTKIT_TOOL_HOST", service + "." + region + f".{sld}.com" - ) # temporary host for code run tool - scheme = getenv("AGENTKIT_TOOL_SCHEME", "https", allow_false_values=True).lower() - if scheme not in {"http", "https"}: - scheme = "https" + tool_id = resolve_agentkit_tool_id("AGENTKIT_TOOL_ID_SCRIPT") + service, region, host, _ = get_agentkit_endpoint_config() logger.debug(f"tools endpoint: {host}") session_id = tool_context._invocation_context.session.id @@ -73,50 +55,14 @@ def run_code( f"Running code in language: {language}, session_id={session_id}, code={code}, tool_id={tool_id}, host={host}, service={service}, region={region}, timeout={timeout}" ) - ak = tool_context.state.get("VOLCENGINE_ACCESS_KEY") - sk = tool_context.state.get("VOLCENGINE_SECRET_KEY") - header = {} - - if not (ak and sk): - logger.debug("Get AK/SK from tool context failed.") - ak = os.getenv("VOLCENGINE_ACCESS_KEY") - sk = os.getenv("VOLCENGINE_SECRET_KEY") - if not (ak and sk): - logger.debug( - "Get AK/SK from environment variables failed. Try to use credential from Iam." - ) - credential = get_credential_from_vefaas_iam() - ak = credential.access_key_id - sk = credential.secret_access_key - header = {"X-Security-Token": credential.session_token} - else: - logger.debug("Successfully get AK/SK from environment variables.") - else: - logger.debug("Successfully get AK/SK from tool context.") - - res = ve_request( - request_body={ - "ToolId": tool_id, - "UserSessionId": tool_user_session_id, - "OperationType": "RunCode", - "OperationPayload": json.dumps( - { - "code": code, - "timeout": timeout, - "kernel_name": language, - } - ), - "Ttl": os.getenv("AGENTKIT_TOOL_TTL", 1800), - }, - action="InvokeTool", - ak=ak, - sk=sk, - service=service, - version="2025-10-30", - region=region, - host=host, - header=header, - scheme=scheme, + res = invoke_agentkit_run_code( + tool_id=tool_id, + tool_user_session_id=tool_user_session_id, + code=code, + timeout=timeout, + kernel_name=language, + tool_state=tool_context.state if tool_context else None, + ttl=int(os.getenv("AGENTKIT_TOOL_TTL", "1800")), ) logger.debug(f"Invoke run code response: {res}") diff --git a/veadk/tools/builtin_tools/run_sandbox_agent.py b/veadk/tools/builtin_tools/run_sandbox_agent.py new file mode 100644 index 00000000..d5339d64 --- /dev/null +++ b/veadk/tools/builtin_tools/run_sandbox_agent.py @@ -0,0 +1,203 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from typing import Optional + +from google.adk.tools import ToolContext + +from veadk.tools.builtin_tools._agentkit import invoke_agentkit_run_code +from veadk.utils.logger import get_logger + +logger = get_logger(__name__) + + +def _clean_ansi_codes(text: str) -> str: + """Remove ANSI escape sequences (color codes, etc.).""" + import re + + ansi_escape = re.compile(r"\x1b\[[0-9;]*m") + return ansi_escape.sub("", text) + + +def _format_execution_result(result_str: str) -> str: + """Format execution result and normalize sandbox output.""" + try: + result_json = json.loads(result_str) + + if not result_json.get("success"): + message = result_json.get("message", "Unknown error") + outputs = result_json.get("data", {}).get("outputs", []) + if outputs and isinstance(outputs[0], dict): + error_msg = outputs[0].get("ename", "Unknown error") + return f"Execution failed: {message}, {error_msg}" + + outputs = result_json.get("data", {}).get("outputs", []) + if not outputs: + return "No output generated" + + formatted_lines = [] + for output in outputs: + if output and isinstance(output, dict) and "text" in output: + text = output["text"] + text = _clean_ansi_codes(text) + text = text.replace("\\n", "\n") + formatted_lines.append(text) + + return "".join(formatted_lines).strip() + + except json.JSONDecodeError: + return _clean_ansi_codes(result_str) + except Exception as e: + logger.warning(f"Error formatting result: {e}, returning raw result") + return result_str + + +def _build_agent_command( + workflow_prompt: str, skills: Optional[list[str]] = None +) -> list[str]: + cmd = ["python", "agent.py", workflow_prompt] + if skills: + cmd.extend(["--skills"] + skills) + return cmd + + +def _build_agent_runner_code( + cmd: list[str], + timeout: int, + env_vars: dict[str, str], + working_dir: str = "/home/gem/veadk_skills", +) -> str: + effective_timeout = max(1, timeout - 10) + return f""" +import subprocess +import os +import time +import select +import sys + +env = os.environ.copy() +for key, value in {env_vars!r}.items(): + if key not in env: + env[key] = value + +process = subprocess.Popen( + {cmd!r}, + cwd={working_dir!r}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + bufsize=1, + universal_newlines=True +) + +start_time = time.time() +timeout = {effective_timeout} + +with open('/tmp/agent.log', 'w') as log_file: + while True: + if time.time() - start_time > timeout: + process.kill() + log_file.write('log_type=stderr request_id=x function_id=y revision_number=1 Process timeout\\n') + print("Process timeout", end='', file=sys.stderr) + break + + reads = [process.stdout.fileno(), process.stderr.fileno()] + ret = select.select(reads, [], [], 1) + + for fd in ret[0]: + if fd == process.stdout.fileno(): + line = process.stdout.readline() + if line: + log_file.write(f'log_type=stdout request_id=x function_id=y revision_number=1 {{line}}') + log_file.flush() + print(line, end='') + if fd == process.stderr.fileno(): + line = process.stderr.readline() + if line: + log_file.write(f'log_type=stderr request_id=x function_id=y revision_number=1 {{line}}') + log_file.flush() + print(line, end='', file=sys.stderr) + + if process.poll() is not None: + break + + for line in process.stdout: + log_file.write(f'log_type=stdout request_id=x function_id=y revision_number=1 {{line}}') + print(line, end='') + for line in process.stderr: + log_file.write(f'log_type=stderr request_id=x function_id=y revision_number=1 {{line}}') + print(line, end='', file=sys.stderr) +""" + + +def run_sandbox_agent( + workflow_prompt: str, + tool_id: str, + tool_context: ToolContext = None, + skills: Optional[list[str]] = None, + timeout: int = 900, + working_dir: str = "/home/gem/veadk_skills", + extra_env_vars: Optional[dict[str, str]] = None, +) -> str: + """Run a remote sandbox agent with an explicit tool_id.""" + if tool_context is None: + raise ValueError("tool_context is required for run_sandbox_agent") + + session_id = tool_context._invocation_context.session.id + agent_name = tool_context._invocation_context.agent.name + user_id = tool_context._invocation_context.user_id + tool_user_session_id = agent_name + "_" + user_id + "_" + session_id + logger.debug(f"tool_user_session_id: {tool_user_session_id}") + + env_vars = { + "TOOL_USER_SESSION_ID": tool_user_session_id, + "PYTHONPATH": "$SRV_PYTHONPATH:$PYTHONPATH", + } + skill_space_id = os.getenv("SKILL_SPACE_ID", "") + if skill_space_id: + env_vars["SKILL_SPACE_ID"] = skill_space_id + if extra_env_vars: + env_vars.update({k: v for k, v in extra_env_vars.items() if v}) + + logger.debug( + f"Run sandbox agent in session_id={session_id}, tool_id={tool_id}, timeout={timeout}, skills={skills}" + ) + + cmd = _build_agent_command(workflow_prompt=workflow_prompt, skills=skills) + code = _build_agent_runner_code( + cmd=cmd, + timeout=timeout, + env_vars=env_vars, + working_dir=working_dir, + ) + res = invoke_agentkit_run_code( + tool_id=tool_id, + tool_user_session_id=tool_user_session_id, + code=code, + timeout=timeout, + kernel_name="python3", + tool_state=tool_context.state if tool_context else None, + ) + logger.debug(f"Invoke run sandbox agent response: {res}") + + try: + return _format_execution_result(res["Result"]["Result"]) + except KeyError as e: + logger.error( + f"Error occurred while running sandbox agent: {e}, response is {res}" + ) + return res