Skip to content

Commit 5fce720

Browse files
feat: support nacos in agent update (#427)
* feat: support nacos in agent update * update * add auto fetch nacos token
1 parent 743a4d7 commit 5fce720

6 files changed

Lines changed: 305 additions & 2 deletions

File tree

config.yaml.full

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ database:
151151
region: cn-beijing # default Volcengine TOS Vector region
152152
bucket:
153153
account_id:
154-
154+
155+
# Dynamic config
156+
nacos:
157+
endpoint:
158+
password:
155159

156160

157161
# [optional] for prompt optimization in cli/app

veadk/agent.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
if not os.getenv("LITELLM_LOCAL_MODEL_COST_MAP"):
2525
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
2626

27+
import uuid
28+
2729
from google.adk.agents import LlmAgent, RunConfig
2830
from google.adk.agents.base_agent import BaseAgent
2931
from google.adk.agents.context_cache_config import ContextCacheConfig
@@ -89,6 +91,7 @@ class Agent(LlmAgent):
8991

9092
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
9193

94+
id: str = Field(default_factory=lambda: str(uuid.uuid4()).split("-")[0])
9295
name: str = DEFAULT_AGENT_NAME
9396
description: str = DEFAULT_DESCRIPTION
9497
instruction: Union[str, InstructionProvider] = DEFAULT_INSTRUCTION
@@ -278,7 +281,13 @@ def model_post_init(self, __context: Any) -> None:
278281

279282
logger.info(f"{self.__class__.__name__} `{self.name}` init done.")
280283
logger.debug(
281-
f"Agent: {self.model_dump(include={'name', 'model_name', 'model_api_base', 'tools'})}"
284+
f"Agent: {self.model_dump(include={'id', 'name', 'model_name', 'model_api_base', 'tools'})}"
285+
)
286+
287+
def update_model(self, model_name: str):
288+
logger.info(f"Updating model to {model_name}")
289+
self.model = self.model.model_copy(
290+
update={"model": f"{self.model_provider}/{model_name}"}
282291
)
283292

284293
async def _run(

veadk/auth/veauth/mse_veauth.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
18+
from veadk.configs.database_configs import MSENacosConfig
19+
from veadk.utils.logger import get_logger
20+
from veadk.utils.volcengine_sign import ve_request
21+
22+
logger = get_logger(__name__)
23+
24+
25+
def get_instance_id_by_name(instance_name: str, region: str, project_name: str) -> str:
26+
logger.info(f"Fetching MSE Nacos instance id by instance name {instance_name} ...")
27+
28+
access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
29+
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
30+
session_token = ""
31+
32+
if not (access_key and secret_key):
33+
# try to get from vefaas iam
34+
cred = get_credential_from_vefaas_iam()
35+
access_key = cred.access_key_id
36+
secret_key = cred.secret_access_key
37+
session_token = cred.session_token
38+
39+
res = ve_request(
40+
request_body={
41+
"Filter": {"Status": [], "ProjectName": project_name},
42+
"PageNumber": 1,
43+
"PageSize": 10,
44+
"ProjectName": project_name,
45+
},
46+
header={"X-Security-Token": session_token},
47+
action="ListNacosRegistries",
48+
ak=access_key,
49+
sk=secret_key,
50+
service="mse",
51+
version="2022-01-01",
52+
region=region,
53+
host="open.volcengineapi.com",
54+
)
55+
56+
try:
57+
for item in res["Result"]["Items"]:
58+
if item["Name"] == instance_name:
59+
logger.info(
60+
f"Found MSE Nacos instance id {item['Id']} by instance name {instance_name}"
61+
)
62+
return item["Id"]
63+
raise ValueError(f"Id by instance name {instance_name} not found: {res}")
64+
except Exception as e:
65+
logger.error(
66+
f"Failed to get MSE Nacos instance id by name {instance_name}: {e}, response: {res}"
67+
)
68+
raise e
69+
70+
71+
def get_mse_cridential(
72+
instance_name: str,
73+
region: str = "cn-beijing",
74+
project_name: str = "default",
75+
) -> MSENacosConfig:
76+
logger.info("Fetching MSE Nacos token...")
77+
78+
access_key = os.getenv("VOLCENGINE_ACCESS_KEY")
79+
secret_key = os.getenv("VOLCENGINE_SECRET_KEY")
80+
session_token = ""
81+
82+
if not (access_key and secret_key):
83+
# try to get from vefaas iam
84+
cred = get_credential_from_vefaas_iam()
85+
access_key = cred.access_key_id
86+
secret_key = cred.secret_access_key
87+
session_token = cred.session_token
88+
89+
instance_id = get_instance_id_by_name(
90+
instance_name=instance_name, region=region, project_name=project_name
91+
)
92+
93+
res = ve_request(
94+
request_body={
95+
"Id": instance_id,
96+
"ProjectName": project_name,
97+
},
98+
header={"X-Security-Token": session_token},
99+
action="GetNacosRegistry",
100+
ak=access_key,
101+
sk=secret_key,
102+
service="mse",
103+
version="2022-01-01",
104+
region=region,
105+
host="open.volcengineapi.com",
106+
)
107+
108+
try:
109+
logger.info(
110+
f"Successfully fetched MSE Nacos endpoint {res['Result']['NacosRegistry']['PublicAddress']} and corresponding password."
111+
)
112+
return MSENacosConfig(
113+
endpoint=res["Result"]["NacosRegistry"]["PublicAddress"],
114+
password=res["Result"]["NacosRegistry"]["InitialPassword"],
115+
)
116+
except Exception as e:
117+
logger.error(f"Failed to get MSE Nacos token: {e}, response: {res}")
118+
raise e

veadk/configs/database_configs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,13 @@ class TOSVectorConfig(BaseSettings):
178178
user_agent_soft_version: str | None = None
179179

180180
user_agent_customized_key_values: dict[str, str] | None = None
181+
182+
183+
class MSENacosConfig(BaseSettings):
184+
model_config = SettingsConfigDict(env_prefix="NACOS_")
185+
186+
endpoint: str
187+
port: str = "8848" # hard coding by Volcengine MSE Nacos service
188+
189+
username: str = "nacos" # hard coding by Volcengine MSE Nacos service
190+
password: str
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import os
17+
18+
from v2.nacos import ClientConfig, NacosConfigService
19+
from v2.nacos.config.model.config_param import ConfigParam
20+
21+
from veadk.agent import Agent
22+
from veadk.auth.veauth.mse_veauth import get_mse_cridential
23+
from veadk.consts import DEFAULT_NACOS_GROUP, DEFAULT_NACOS_INSTANCE_NAME
24+
from veadk.utils.logger import get_logger
25+
26+
logger = get_logger(__name__)
27+
28+
29+
class DynamicConfigManager:
30+
"""
31+
DynamicConfigManager is responsible for creating and publishing dynamic config to nacos.
32+
"""
33+
34+
def __init__(
35+
self,
36+
agents: list[Agent] | Agent,
37+
):
38+
"""
39+
Initialize DynamicConfigManager with agents and app_name.
40+
41+
Args:
42+
agents (list[Agent] | Agent): The agent(s) to be included in the dynamic config.
43+
"""
44+
if isinstance(agents, list):
45+
self.agents = agents
46+
else:
47+
self.agents = [agents]
48+
49+
logger.debug(f"DynamicConfigManager init with {len(self.agents)} agent(s).")
50+
51+
async def create_config(
52+
self,
53+
configs: dict = {},
54+
instance_name: str = "",
55+
group_id: str = "",
56+
):
57+
if not instance_name:
58+
logger.warning(
59+
f"instance_name is not provided, use default value `{DEFAULT_NACOS_INSTANCE_NAME}`. This may lead to unexpected behavior such as configuration override."
60+
)
61+
instance_name = DEFAULT_NACOS_INSTANCE_NAME
62+
63+
if not group_id:
64+
logger.warning(
65+
f"group_id is not provided, use default value `{DEFAULT_NACOS_GROUP}`. This may lead to unexpected behavior such as configuration override."
66+
)
67+
group_id = group_id or DEFAULT_NACOS_GROUP
68+
69+
nacos_endpoint = os.getenv("NACOS_ENDPOINT")
70+
nacos_port = os.getenv("NACOS_PORT", "8848")
71+
nacos_username = os.getenv("NACOS_USERNAME", "nacos")
72+
nacos_password = os.getenv("NACOS_PASSWORD")
73+
74+
if not all([nacos_endpoint, nacos_port, nacos_username, nacos_password]):
75+
logger.warning(
76+
"fetch NACOS_ENDPOINT, NACOS_PORT, NACOS_USERNAME, and NACOS_PASSWORD from env failed, try to get by volcengine AK/SK."
77+
)
78+
79+
nacos_credentials = get_mse_cridential(instance_name=instance_name)
80+
nacos_endpoint = nacos_credentials.endpoint
81+
nacos_port = nacos_credentials.port
82+
nacos_username = nacos_credentials.username
83+
nacos_password = nacos_credentials.password
84+
85+
client_config = ClientConfig(
86+
server_addresses=f"{nacos_endpoint}:{nacos_port}",
87+
namespace_id="",
88+
username=nacos_username,
89+
password=nacos_password,
90+
)
91+
92+
config_client = await NacosConfigService.create_config_service(
93+
client_config=client_config
94+
)
95+
96+
if not configs:
97+
logger.info("user config_dict is empty, use default config instead.")
98+
configs = {
99+
"agent": [
100+
{
101+
"id": agent.id,
102+
"name": agent.name,
103+
"description": agent.description,
104+
"model_name": agent.model_name,
105+
"instruction": agent.instruction,
106+
}
107+
for agent in self.agents
108+
]
109+
}
110+
response = await config_client.publish_config(
111+
param=ConfigParam(
112+
data_id="veadk",
113+
group=group_id,
114+
type="json",
115+
content=json.dumps(configs),
116+
)
117+
)
118+
assert response, "publish config to nacos failed"
119+
logger.info("Publish config to nacos success")
120+
121+
await config_client.add_listener(
122+
data_id="veadk",
123+
group="VEADK_GROUP",
124+
listener=self.handle_config_update,
125+
)
126+
logger.info("Add config listener to nacos success")
127+
128+
return config_client
129+
130+
def register_agent(self, agent: list[Agent] | Agent):
131+
if isinstance(agent, list):
132+
self.agents.extend(agent)
133+
else:
134+
self.agents.append(agent)
135+
136+
def update_agent(self, configs: dict):
137+
for agent in self.agents:
138+
for config in configs["agent"]:
139+
if agent.id == config["id"]:
140+
logger.info(f"Update agent {agent.id} with config {config}")
141+
name = config["name"]
142+
description = config["description"]
143+
model_name = config["model_name"]
144+
instruction = config["instruction"]
145+
146+
agent.name = name
147+
agent.description = description
148+
if model_name != agent.model_name:
149+
agent.update_model(model_name=model_name)
150+
agent.instruction = instruction
151+
152+
async def handle_config_update(self, tenant, data_id, group, content) -> None:
153+
logger.debug(
154+
"listen, tenant:{} data_id:{} group:{} content:{}".format(
155+
tenant, data_id, group, content
156+
)
157+
)
158+
content = json.loads(content)
159+
self.update_agent(content)

veadk/consts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,6 @@
7272
DEFAULT_IMAGE_GENERATE_MODEL_API_BASE = "https://ark.cn-beijing.volces.com/api/v3/"
7373

7474
VEFAAS_IAM_CRIDENTIAL_PATH = "/var/run/secrets/iam/credential"
75+
76+
DEFAULT_NACOS_GROUP = "VEADK_GROUP"
77+
DEFAULT_NACOS_INSTANCE_NAME = "veadk"

0 commit comments

Comments
 (0)