From 1d1ee54b709f59aa9e919bdcdd995f9bc8850a6e Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 7 Aug 2025 09:26:28 +0800 Subject: [PATCH 1/2] refine deploy cli and cloud app --- veadk/cli/main.py | 80 ++++++++++++++- veadk/cli/services/vefaas/template/deploy.py | 20 ++-- veadk/cloud/cloud_agent_engine.py | 102 ++++++++----------- veadk/cloud/cloud_app.py | 85 +++++++++++++--- 4 files changed, 203 insertions(+), 84 deletions(-) diff --git a/veadk/cli/main.py b/veadk/cli/main.py index cda57c89..1c2372f0 100644 --- a/veadk/cli/main.py +++ b/veadk/cli/main.py @@ -32,6 +32,32 @@ app = typer.Typer(name="vego") +def set_variable_in_file(file_path: str, setting_values: dict): + import ast + + with open(file_path, "r", encoding="utf-8") as f: + source_code = f.read() + + tree = ast.parse(source_code) + + class VariableTransformer(ast.NodeTransformer): + def visit_Assign(self, node: ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id in setting_values: + node.value = ast.Constant(value=setting_values[target.id]) + return node + + transformer = VariableTransformer() + new_tree = transformer.visit(tree) + ast.fix_missing_locations(new_tree) + new_source_code = ast.unparse(new_tree) + + with open(file_path, "w", encoding="utf-8") as f: + f.write(new_source_code) + + print("Your project has beed created.") + + @app.command() def init(): """Init a veadk project that can be deployed to Volcengine VeFaaS.""" @@ -40,9 +66,9 @@ def init(): cwd = Path.cwd() template_dir = Path(__file__).parent.resolve() / "services" / "vefaas" / "template" - name = Prompt.ask("Project name", default="veadk-cloud-agent") + local_dir_name = Prompt.ask("Local directory name", default="veadk-cloud-proj") - target_dir = cwd / name + target_dir = cwd / local_dir_name if target_dir.exists(): response = Confirm.ask( @@ -52,11 +78,57 @@ def init(): print("Operation cancelled.") return else: - shutil.rmtree(target_dir) # 删除旧目录 + shutil.rmtree(target_dir) print(f"Deleted existing directory: {target_dir}") + vefaas_application_name = Prompt.ask( + "Volcengine FaaS application name", default="veadk-cloud-agent" + ) + + gateway_name = Prompt.ask( + "Volcengine gateway instance name", default="", show_default=True + ) + + gateway_service_name = Prompt.ask( + "Volcengine gateway service name", default="", show_default=True + ) + + gateway_upstream_name = Prompt.ask( + "Volcengine gateway upstream name", default="", show_default=True + ) + + deploy_mode_options = { + "1": "A2A Server", + "2": "VeADK Studio", + "3": "VeADK Web / Google ADK Web", + } + + deploy_mode = Prompt.ask( + """Choose your deploy mode: +1. A2A Server +2. VeADK Studio +3. VeADK Web / Google ADK Web +""", + default="1", + ) + + if deploy_mode in deploy_mode_options: + deploy_mode = deploy_mode_options[deploy_mode] + else: + print("Invalid deploy mode, set default to A2A Server") + deploy_mode = deploy_mode_options["1"] + + setting_values = { + "VEFAAS_APPLICATION_NAME": vefaas_application_name, + "GATEWAY_NAME": gateway_name, + "GATEWAY_SERVICE_NAME": gateway_service_name, + "GATEWAY_UPSTREAM_NAME": gateway_upstream_name, + "USE_STUDIO": deploy_mode == deploy_mode_options["2"], + "USE_ADK_WEB": deploy_mode == deploy_mode_options["3"], + } + shutil.copytree(template_dir, target_dir) - print(f"Created new project: {name}") + set_variable_in_file(target_dir / "deploy.py", setting_values) # @app.command() diff --git a/veadk/cli/services/vefaas/template/deploy.py b/veadk/cli/services/vefaas/template/deploy.py index 6b8de282..72b6f341 100644 --- a/veadk/cli/services/vefaas/template/deploy.py +++ b/veadk/cli/services/vefaas/template/deploy.py @@ -20,34 +20,38 @@ SESSION_ID = "cloud_app_test_session" USER_ID = "cloud_app_test_user" +VEFAAS_APPLICATION_NAME = "weather-reporter" +GATEWAY_NAME = "" +GATEWAY_SERVICE_NAME = "" +GATEWAY_UPSTREAMNAME = "" USE_STUDIO = False USE_ADK_WEB = False async def main(): engine = CloudAgentEngine() + cloud_app = engine.deploy( path=str(Path(__file__).parent / "src"), - name="weather-reporter", # <--- set your application name + application_name=VEFAAS_APPLICATION_NAME, + gateway_name=GATEWAY_NAME, + gateway_service_name=GATEWAY_SERVICE_NAME, + gateway_upstream_name=GATEWAY_UPSTREAMNAME, use_studio=USE_STUDIO, use_adk_web=USE_ADK_WEB, - # gateway_name="", # <--- set your gateway instance name if you have one ) if not USE_STUDIO and not USE_ADK_WEB: response_message = await cloud_app.message_send( "How is the weather like in Beijing?", SESSION_ID, USER_ID ) - + print(f"VeFaaS application ID: {cloud_app.vefaas_application_id}") print(f"Message ID: {response_message.messageId}") - print( - f"Response from {cloud_app.endpoint}: {response_message.parts[0].root.text}" + f"Response from {cloud_app.vefaas_endpoint}: {response_message.parts[0].root.text}" ) - - print(f"App ID: {cloud_app.app_id}") else: - print(f"VeADK Studio URL: {cloud_app.endpoint}") + print(f"Web is running at: {cloud_app.vefaas_endpoint}") if __name__ == "__main__": diff --git a/veadk/cloud/cloud_agent_engine.py b/veadk/cloud/cloud_agent_engine.py index 8d4b1d0a..7b85f8ab 100644 --- a/veadk/cloud/cloud_agent_engine.py +++ b/veadk/cloud/cloud_agent_engine.py @@ -40,71 +40,54 @@ def model_post_init(self, context: Any, /) -> None: ) def _prepare(self, path: str, name: str): - # VeFaaS path check - if "_" in name: - raise ValueError( - f"Invalid Volcengine FaaS function name `{name}`, please use lowercase letters and numbers, or replace it with a `-` char." - ) - - # project path check + # basic check assert os.path.exists(path), f"Local agent project path `{path}` not exists." assert os.path.isdir(path), ( f"Local agent project path `{path}` is not a directory." ) - assert os.path.exists(os.path.join(path, "agent.py")), ( - f"Local agent project path `{path}` does not contain `agent.py` file. Please prepare it according to veadk-python/cloud/template/agent.py.example" - ) - - if os.path.exists(os.path.join(path, "app.py")): - logger.warning( - f"Local agent project path `{path}` contains an `app.py` file. Use your own `app.py` file may cause unexpected behavior." - ) - else: - logger.info( - f"No `app.py` detected in local agent project path `{path}`. Prepare it." - ) - template_app_py = ( - f"{Path(__file__).resolve().parent.resolve()}/template/app.py" - ) - import shutil - - shutil.copy(template_app_py, os.path.join(path, "app.py")) - - if os.path.exists(os.path.join(path, "studio_app.py")): - logger.warning( - f"Local agent project path `{path}` contains an `studio_app.py` file. Use your own `studio_app.py` file may cause unexpected behavior." - ) - else: - logger.info( - f"No `studio_app.py` detected in local agent project path `{path}`. Prepare it." - ) - template_studio_app_py = ( - f"{Path(__file__).resolve().parent.resolve()}/template/studio_app.py" + # VeFaaS application/function name check + if "_" in name: + raise ValueError( + f"Invalid Volcengine FaaS function name `{name}`, please use lowercase letters and numbers, or replace it with a `-` char." ) - import shutil - shutil.copy(template_studio_app_py, os.path.join(path, "studio_app.py")) + # project structure check + assert os.path.exists(os.path.join(path, "agent.py")), ( + f"Local agent project path `{path}` does not contain `agent.py` file. Please prepare it according to our document https://volcengine.github.io/veadk-python/deploy.html" + ) - if os.path.exists(os.path.join(path, "run.sh")): + if not os.path.exists(os.path.join(path, "config.yaml")): logger.warning( - f"Local agent project path `{path}` contains a `run.sh` file. Use your own `run.sh` file may cause unexpected behavior." - ) - else: - logger.info( - f"No `run.sh` detected in local agent project path `{path}`. Prepare it." - ) - template_run_sh = ( - f"{Path(__file__).resolve().parent.resolve()}/template/run.sh" + f"Local agent project path `{path}` does not contain `config.yaml` file. Some important config items may not be set." ) - import shutil - shutil.copy(template_run_sh, os.path.join(path, "run.sh")) + # prepare template files if not have + template_files = [ + "app.py", + "studio_app.py", + "run.sh", + "requirements.txt", + "__init__.py", + ] + for template_file in template_files: + if os.path.exists(os.path.join(path, template_file)): + logger.warning( + f"Local agent project path `{path}` contains a `{template_file}` file. Use your own `{template_file}` file may cause unexpected behavior." + ) + else: + logger.info( + f"No `{template_file}` detected in local agent project path `{path}`. Prepare it." + ) + template_file_path = f"{Path(__file__).resolve().parent.resolve().parent.resolve()}/cli/services/vefaas/template/src/{template_file}" + import shutil + + shutil.copy(template_file_path, os.path.join(path, template_file)) def deploy( self, + application_name: str, path: str, - name: str, gateway_name: str = "", gateway_service_name: str = "", gateway_upstream_name: str = "", @@ -123,14 +106,13 @@ def deploy( assert not (use_studio and use_adk_web), ( "use_studio and use_adk_web can not be True at the same time." ) + # prevent deepeval writing operations import veadk.config veadk.config.veadk_environments["DEEPEVAL_TELEMETRY_OPT_OUT"] = "YES" if use_studio: - import veadk.config - veadk.config.veadk_environments["USE_STUDIO"] = "True" else: import veadk.config @@ -148,19 +130,19 @@ def deploy( # convert `path` to absolute path path = str(Path(path).resolve()) - self._prepare(path, name) + self._prepare(path, application_name) if not gateway_name: - gateway_name = f"{name}-gw-{formatted_timestamp()}" + gateway_name = f"{application_name}-gw-{formatted_timestamp()}" if not gateway_service_name: - gateway_service_name = f"{name}-gw-svr-{formatted_timestamp()}" + gateway_service_name = f"{application_name}-gw-svr-{formatted_timestamp()}" if not gateway_upstream_name: - gateway_upstream_name = f"{name}-gw-us-{formatted_timestamp()}" + gateway_upstream_name = f"{application_name}-gw-us-{formatted_timestamp()}" try: vefaas_application_url, app_id, function_id = self._vefaas_service.deploy( path=path, - name=name, + name=application_name, gateway_name=gateway_name, gateway_service_name=gateway_service_name, gateway_upstream_name=gateway_upstream_name, @@ -168,9 +150,9 @@ def deploy( _ = function_id # for future use return CloudApp( - name=name, - endpoint=vefaas_application_url, - app_id=app_id, + vefaas_application_name=application_name, + vefaas_endpoint=vefaas_application_url, + vefaas_application_id=app_id, ) except Exception as e: raise ValueError( diff --git a/veadk/cloud/cloud_app.py b/veadk/cloud/cloud_app.py index 4a59ab37..78447025 100644 --- a/veadk/cloud/cloud_app.py +++ b/veadk/cloud/cloud_app.py @@ -19,6 +19,7 @@ from a2a.client import A2ACardResolver, A2AClient from a2a.types import AgentCard, Message, MessageSendParams, SendMessageRequest +from veadk.config import getenv from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -35,19 +36,34 @@ class CloudApp: def __init__( self, - name: str, - endpoint: str, - app_id: str, + vefaas_application_name: str, + vefaas_endpoint: str, + vefaas_application_id: str, use_agent_card: bool = False, ): - self.endpoint = endpoint - self.app_id = app_id - self.name = name + self.vefaas_endpoint = vefaas_endpoint + self.vefaas_application_id = vefaas_application_id + self.vefaas_application_name = vefaas_application_name self.use_agent_card = use_agent_card - if not endpoint.startswith("http") and not endpoint.startswith("https"): + # vefaas must be set one of three + if ( + not vefaas_endpoint + and not vefaas_application_id + and not vefaas_application_name + ): raise ValueError( - f"Invalid endpoint: {endpoint}. The endpoint must start with `http` or `https`." + "VeFaaS CloudAPP must be set one of endpoint, application_id, or application_name." + ) + + if not vefaas_endpoint: + self.vefaas_endpoint = self._get_vefaas_endpoint() + + if not self.vefaas_endpoint.startswith( + "http" + ) and not self.vefaas_endpoint.startswith("https"): + raise ValueError( + f"Invalid endpoint: {vefaas_endpoint}. The endpoint must start with `http` or `https`." ) if use_agent_card: @@ -57,6 +73,28 @@ def __init__( self.httpx_client = httpx.AsyncClient() + def _get_vefaas_endpoint(self) -> str: + vefaas_endpoint = "" + + if self.vefaas_application_id: + # TODO(zakahan): get endpoint from vefaas + vefaas_endpoint = ... + return vefaas_endpoint + + if self.vefaas_application_name: + # TODO(zakahan): get endpoint from vefaas + vefaas_endpoint = ... + return vefaas_endpoint + + def _get_vefaas_application_id_by_name(self) -> str: + if not self.vefaas_application_name: + raise ValueError( + "VeFaaS CloudAPP must be set application_name to get application_id." + ) + # TODO(zakahan): get application id from vefaas application name + vefaas_application_id = "" + return vefaas_application_id + async def _get_a2a_client(self) -> A2AClient: if self.use_agent_card: async with self.httpx_client as httpx_client: @@ -76,8 +114,30 @@ async def _get_a2a_client(self) -> A2AClient: else: return A2AClient(httpx_client=self.httpx_client, url=self.endpoint) - def delete_self(self, volcengine_ak: str, volcengine_sk: str): - confirm = input(f"Confirm delete cloud app {self.app_id}? (y/N): ") + def update_self( + self, + volcengine_ak: str = getenv("VOLCENGINE_ACCESS_KEY"), + volcengine_sk: str = getenv("VOLCENGINE_SECRET_KEY"), + ): + if not volcengine_ak or not volcengine_sk: + raise ValueError("Volcengine access key and secret key must be set.") + + # TODO(floritange): support update cloud app + + def delete_self( + self, + volcengine_ak: str = getenv("VOLCENGINE_ACCESS_KEY"), + volcengine_sk: str = getenv("VOLCENGINE_SECRET_KEY"), + ): + if not volcengine_ak or not volcengine_sk: + raise ValueError("Volcengine access key and secret key must be set.") + + if not self.vefaas_application_id: + self.vefaas_application_id = self._get_vefaas_application_id_by_name() + + confirm = input( + f"Confirm delete cloud app {self.vefaas_application_id}? (y/N): " + ) if confirm.lower() != "y": print("Delete cancelled.") return @@ -85,8 +145,8 @@ def delete_self(self, volcengine_ak: str, volcengine_sk: str): from veadk.cli.services.vefaas.vefaas import VeFaaS vefaas_client = VeFaaS(access_key=volcengine_ak, secret_key=volcengine_sk) - vefaas_client.delete(self.app_id) - print(f"Cloud app {self.app_id} is deleting...") + vefaas_client.delete(self.vefaas_application_id) + print(f"Cloud app {self.vefaas_application_id} is deleting...") async def message_send( self, message: str, session_id: str, user_id: str, timeout: float = 600.0 @@ -119,5 +179,6 @@ async def message_send( ) return res.root.result except Exception as e: + # TODO(floritange): show error log on VeFaaS function print(e) return None From d744b3e31902fbcacb98ff67f2e62c31736ff63e Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 7 Aug 2025 09:37:49 +0800 Subject: [PATCH 2/2] remove cloud template --- veadk/cloud/template/agent.py | 20 --------- veadk/cloud/template/app.py | 66 ------------------------------ veadk/cloud/template/run.sh | 63 ---------------------------- veadk/cloud/template/studio_app.py | 47 --------------------- 4 files changed, 196 deletions(-) delete mode 100644 veadk/cloud/template/agent.py delete mode 100644 veadk/cloud/template/app.py delete mode 100644 veadk/cloud/template/run.sh delete mode 100644 veadk/cloud/template/studio_app.py diff --git a/veadk/cloud/template/agent.py b/veadk/cloud/template/agent.py deleted file mode 100644 index 3cb2a081..00000000 --- a/veadk/cloud/template/agent.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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 veadk.agent import Agent -from veadk.memory.short_term_memory import ShortTermMemory - -agent: Agent = ... -app_name: str = ... -short_term_memory: ShortTermMemory = ... -root_agent = agent diff --git a/veadk/cloud/template/app.py b/veadk/cloud/template/app.py deleted file mode 100644 index 0a2d0c09..00000000 --- a/veadk/cloud/template/app.py +++ /dev/null @@ -1,66 +0,0 @@ -# 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 os - -from agent import agent, app_name, short_term_memory -from veadk.a2a.ve_a2a_server import init_app -from veadk.tracing.base_tracer import BaseTracer -from veadk.tracing.telemetry.opentelemetry_tracer import OpentelemetryTracer - - -# ============================================================================== -# Tracer Config ================================================================ - -TRACERS: list[BaseTracer] = [] - -exporters = [] -if os.getenv("VEADK_TRACER_APMPLUS", "").lower() == "true": - from veadk.tracing.telemetry.exporters.apmplus_exporter import APMPlusExporter - - exporters.append(APMPlusExporter()) - -if os.getenv("VEADK_TRACER_COZELOOP", "").lower() == "true": - from veadk.tracing.telemetry.exporters.cozeloop_exporter import CozeloopExporter - - exporters.append(CozeloopExporter()) - -if os.getenv("VEADK_TRACER_TLS", "").lower() == "true": - from veadk.tracing.telemetry.exporters.tls_exporter import TLSExporter - - exporters.append(TLSExporter()) - -TRACERS.append(OpentelemetryTracer(exporters=exporters)) - - -agent.tracers.extend(TRACERS) -if not getattr(agent, "before_model_callback", None): - agent.before_model_callback = [] -if not getattr(agent, "after_model_callback", None): - agent.after_model_callback = [] -for tracer in TRACERS: - if tracer.llm_metrics_hook not in agent.before_model_callback: - agent.before_model_callback.append(tracer.llm_metrics_hook) - if tracer.token_metrics_hook not in agent.after_model_callback: - agent.after_model_callback.append(tracer.token_metrics_hook) - -# Tracer Config ================================================================ -# ============================================================================== - -app = init_app( - server_url="0.0.0.0", # Automatic identification is not supported yet. - app_name=app_name, - agent=agent, - short_term_memory=short_term_memory, -) diff --git a/veadk/cloud/template/run.sh b/veadk/cloud/template/run.sh deleted file mode 100644 index dc5b268d..00000000 --- a/veadk/cloud/template/run.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -set -ex -cd `dirname $0` - -# A special check for CLI users (run.sh should be located at the 'root' dir) -if [ -d "output" ]; then - cd ./output/ -fi - -# Default values for host and port -HOST="0.0.0.0" -PORT=${_FAAS_RUNTIME_PORT:-8000} -TIMEOUT=${_FAAS_FUNC_TIMEOUT} - -export SERVER_HOST=$HOST -export SERVER_PORT=$PORT - -export PYTHONPATH=$PYTHONPATH:./site-packages -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - --port) - PORT="$2" - shift 2 - ;; - --host) - HOST="$2" - shift 2 - ;; - *) - shift - ;; - esac -done - -# in case of uvicorn and fastapi not installed in user's requirements.txt -python3 -m pip install uvicorn[standard] - -python3 -m pip install fastapi - -USE_STUDIO=${USE_STUDIO:-False} -USE_ADK_WEB=${USE_ADK_WEB:-False} - -if [ "$USE_STUDIO" = "True" ]; then - echo "USE_STUDIO is True, running veadk studio" - # running veadk studio - exec python3 -m uvicorn studio_app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio -elif [ "$USE_STUDIO" = "False" ]; then - echo "USE_STUDIO is False" - - if [ "$USE_ADK_WEB" = "True" ]; then - echo "USE_ADK_WEB is True, running veadk web" - # running veadk web - cd ../ - exec python3 -m veadk.cli.main web --host "0.0.0.0" - else - echo "USE_ADK_WEB is False, running a2a server" - exec python3 -m uvicorn app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio - fi -else - # running a2a server - exec python3 -m uvicorn app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio -fi \ No newline at end of file diff --git a/veadk/cloud/template/studio_app.py b/veadk/cloud/template/studio_app.py deleted file mode 100644 index 9e36effe..00000000 --- a/veadk/cloud/template/studio_app.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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 os -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path - -from veadk.cli.studio.fast_api import get_fast_api_app - -path = Path(__file__).parent.resolve() - -agent_py_path = os.path.join(path, "agent.py") -if not os.path.exists(agent_py_path): - raise FileNotFoundError(f"agent.py not found in {path}") - -spec = spec_from_file_location("agent", agent_py_path) -if spec is None: - raise ImportError(f"Could not load spec for agent from {agent_py_path}") - -module = module_from_spec(spec) - -try: - spec.loader.exec_module(module) -except Exception as e: - raise ImportError(f"Failed to execute agent.py: {e}") - -agent = None -short_term_memory = None -try: - agent = module.agent - short_term_memory = module.short_term_memory -except AttributeError as e: - missing = str(e).split("'")[1] if "'" in str(e) else "unknown" - raise AttributeError(f"agent.py is missing required variable: {missing}") - -app = get_fast_api_app(agent, short_term_memory)