agentscope-runtime 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentscope_runtime/adapters/agentscope/stream.py +2 -9
- agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
- agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
- agentscope_runtime/adapters/utils.py +6 -0
- agentscope_runtime/cli/commands/deploy.py +383 -0
- agentscope_runtime/common/collections/redis_mapping.py +4 -1
- agentscope_runtime/common/container_clients/knative_client.py +466 -0
- agentscope_runtime/engine/__init__.py +4 -0
- agentscope_runtime/engine/app/agent_app.py +48 -5
- agentscope_runtime/engine/constant.py +1 -0
- agentscope_runtime/engine/deployers/__init__.py +12 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +31 -1
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +458 -41
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +76 -0
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +749 -0
- agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
- agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
- agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
- agentscope_runtime/engine/deployers/kubernetes_deployer.py +3 -0
- agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +8 -2
- agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +5 -0
- agentscope_runtime/engine/deployers/utils/net_utils.py +65 -0
- agentscope_runtime/engine/runner.py +17 -3
- agentscope_runtime/engine/schemas/exception.py +24 -0
- agentscope_runtime/engine/services/agent_state/redis_state_service.py +61 -8
- agentscope_runtime/engine/services/agent_state/state_service_factory.py +2 -5
- agentscope_runtime/engine/services/memory/redis_memory_service.py +129 -25
- agentscope_runtime/engine/services/session_history/redis_session_history_service.py +160 -34
- agentscope_runtime/engine/tracing/wrapper.py +18 -4
- agentscope_runtime/sandbox/__init__.py +14 -6
- agentscope_runtime/sandbox/box/base/__init__.py +2 -2
- agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
- agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
- agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
- agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
- agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
- agentscope_runtime/sandbox/box/sandbox.py +98 -65
- agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
- agentscope_runtime/sandbox/build.py +50 -57
- agentscope_runtime/sandbox/client/__init__.py +6 -1
- agentscope_runtime/sandbox/client/async_http_client.py +339 -0
- agentscope_runtime/sandbox/client/base.py +74 -0
- agentscope_runtime/sandbox/client/http_client.py +108 -329
- agentscope_runtime/sandbox/enums.py +7 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +264 -4
- agentscope_runtime/sandbox/manager/server/app.py +7 -1
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/METADATA +109 -29
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/RECORD +58 -46
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/WHEEL +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Nacos Registry Implementation for A2A Protocol
|
|
4
|
+
|
|
5
|
+
This module provides a Nacos-based registry implementation for A2A protocol
|
|
6
|
+
agent service registration. It serves as the default registry implementation
|
|
7
|
+
for A2A protocol adapters.
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Optional, TYPE_CHECKING, List
|
|
15
|
+
|
|
16
|
+
from a2a.types import AgentCard
|
|
17
|
+
from dotenv import find_dotenv, load_dotenv
|
|
18
|
+
from pydantic import ConfigDict
|
|
19
|
+
from pydantic_settings import BaseSettings
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Make the v2.nacos imports optional to avoid hard dependency at
|
|
24
|
+
# module import time.
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from v2.nacos import ClientConfig, ClientConfigBuilder
|
|
27
|
+
from v2.nacos.ai.model.ai_param import (
|
|
28
|
+
RegisterAgentEndpointParam,
|
|
29
|
+
ReleaseAgentCardParam,
|
|
30
|
+
)
|
|
31
|
+
from v2.nacos.ai.nacos_ai_service import NacosAIService
|
|
32
|
+
|
|
33
|
+
_NACOS_SDK_AVAILABLE = True
|
|
34
|
+
else:
|
|
35
|
+
try:
|
|
36
|
+
from v2.nacos import ClientConfig, ClientConfigBuilder
|
|
37
|
+
from v2.nacos.ai.model.ai_param import (
|
|
38
|
+
RegisterAgentEndpointParam,
|
|
39
|
+
ReleaseAgentCardParam,
|
|
40
|
+
)
|
|
41
|
+
from v2.nacos.ai.nacos_ai_service import NacosAIService
|
|
42
|
+
|
|
43
|
+
_NACOS_SDK_AVAILABLE = True
|
|
44
|
+
except Exception:
|
|
45
|
+
logger.warning(
|
|
46
|
+
"[NacosRegistry] Nacos SDK (nacos-sdk-python) is not available. "
|
|
47
|
+
"Install it with: pip install nacos-sdk-python",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
class ClientConfig:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
class ClientConfigBuilder:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
class RegisterAgentEndpointParam:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
class ReleaseAgentCardParam:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
class NacosAIService:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
_NACOS_SDK_AVAILABLE = False
|
|
66
|
+
|
|
67
|
+
# Import after conditional imports to avoid circular dependencies
|
|
68
|
+
# flake8: noqa: E402
|
|
69
|
+
from .a2a_registry import ( # pylint: disable=wrong-import-position
|
|
70
|
+
A2ARegistry,
|
|
71
|
+
A2ATransportsProperties,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NacosSettings(BaseSettings):
|
|
76
|
+
"""Nacos-specific settings loaded from environment variables."""
|
|
77
|
+
|
|
78
|
+
NACOS_SERVER_ADDR: str = "localhost:8848"
|
|
79
|
+
NACOS_USERNAME: Optional[str] = None
|
|
80
|
+
NACOS_PASSWORD: Optional[str] = None
|
|
81
|
+
NACOS_NAMESPACE_ID: Optional[str] = None
|
|
82
|
+
NACOS_ACCESS_KEY: Optional[str] = None
|
|
83
|
+
NACOS_SECRET_KEY: Optional[str] = None
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(
|
|
86
|
+
extra="allow",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_nacos_settings: Optional[NacosSettings] = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_nacos_settings() -> NacosSettings:
|
|
94
|
+
"""Return a singleton Nacos settings instance, loading .env files
|
|
95
|
+
if needed."""
|
|
96
|
+
global _nacos_settings
|
|
97
|
+
|
|
98
|
+
if _nacos_settings is None:
|
|
99
|
+
dotenv_path = find_dotenv(raise_error_if_not_found=False)
|
|
100
|
+
if dotenv_path:
|
|
101
|
+
load_dotenv(dotenv_path, override=False)
|
|
102
|
+
else:
|
|
103
|
+
if os.path.exists(".env.example"):
|
|
104
|
+
load_dotenv(".env.example", override=False)
|
|
105
|
+
_nacos_settings = NacosSettings()
|
|
106
|
+
|
|
107
|
+
return _nacos_settings
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _build_nacos_client_config(settings: NacosSettings) -> Any:
|
|
111
|
+
"""Build Nacos client configuration from settings.
|
|
112
|
+
|
|
113
|
+
Supports both username/password and access key authentication.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
from v2.nacos import ClientConfigBuilder
|
|
117
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"[A2A] Nacos SDK (nacos-sdk-python) is not available. "
|
|
120
|
+
"Install it with: pip install nacos-sdk-python",
|
|
121
|
+
)
|
|
122
|
+
raise ImportError(
|
|
123
|
+
"Nacos SDK (nacos-sdk-python) is not available. "
|
|
124
|
+
"Install it with: pip install nacos-sdk-python",
|
|
125
|
+
) from e
|
|
126
|
+
|
|
127
|
+
builder = ClientConfigBuilder().server_address(settings.NACOS_SERVER_ADDR)
|
|
128
|
+
|
|
129
|
+
if settings.NACOS_NAMESPACE_ID:
|
|
130
|
+
builder.namespace_id(settings.NACOS_NAMESPACE_ID)
|
|
131
|
+
logger.debug(
|
|
132
|
+
"[A2A] Using Nacos namespace: %s",
|
|
133
|
+
settings.NACOS_NAMESPACE_ID,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if settings.NACOS_USERNAME and settings.NACOS_PASSWORD:
|
|
137
|
+
builder.username(settings.NACOS_USERNAME).password(
|
|
138
|
+
settings.NACOS_PASSWORD,
|
|
139
|
+
)
|
|
140
|
+
logger.debug("[A2A] Using Nacos username/password authentication")
|
|
141
|
+
|
|
142
|
+
if settings.NACOS_ACCESS_KEY and settings.NACOS_SECRET_KEY:
|
|
143
|
+
builder.access_key(settings.NACOS_ACCESS_KEY).secret_key(
|
|
144
|
+
settings.NACOS_SECRET_KEY,
|
|
145
|
+
)
|
|
146
|
+
logger.debug("[A2A] Using Nacos access key authentication")
|
|
147
|
+
|
|
148
|
+
return builder.build()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def create_nacos_registry_from_env() -> Optional[A2ARegistry]:
|
|
152
|
+
"""Create a NacosRegistry instance from environment settings.
|
|
153
|
+
|
|
154
|
+
Returns None if the required nacos SDK is not available or
|
|
155
|
+
construction fails.
|
|
156
|
+
"""
|
|
157
|
+
if not _NACOS_SDK_AVAILABLE:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
nacos_settings = get_nacos_settings()
|
|
162
|
+
nacos_client_config = _build_nacos_client_config(nacos_settings)
|
|
163
|
+
registry = NacosRegistry(nacos_client_config=nacos_client_config)
|
|
164
|
+
|
|
165
|
+
auth_methods = []
|
|
166
|
+
if nacos_settings.NACOS_USERNAME and nacos_settings.NACOS_PASSWORD:
|
|
167
|
+
auth_methods.append("username/password")
|
|
168
|
+
if nacos_settings.NACOS_ACCESS_KEY and nacos_settings.NACOS_SECRET_KEY:
|
|
169
|
+
auth_methods.append("access_key")
|
|
170
|
+
auth_status = ", ".join(auth_methods) if auth_methods else "disabled"
|
|
171
|
+
|
|
172
|
+
namespace_info = (
|
|
173
|
+
f", namespace={nacos_settings.NACOS_NAMESPACE_ID}"
|
|
174
|
+
if nacos_settings.NACOS_NAMESPACE_ID
|
|
175
|
+
else ""
|
|
176
|
+
)
|
|
177
|
+
logger.info(
|
|
178
|
+
f"[A2A] Created Nacos registry from environment: "
|
|
179
|
+
f"server={nacos_settings.NACOS_SERVER_ADDR}, "
|
|
180
|
+
f"authentication={auth_status}{namespace_info}",
|
|
181
|
+
)
|
|
182
|
+
return registry
|
|
183
|
+
except (ImportError, ModuleNotFoundError):
|
|
184
|
+
return None
|
|
185
|
+
except Exception:
|
|
186
|
+
logger.warning(
|
|
187
|
+
"[A2A] Failed to construct Nacos registry from settings",
|
|
188
|
+
exc_info=True,
|
|
189
|
+
)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RegistrationStatus(Enum):
|
|
194
|
+
"""Registration task status."""
|
|
195
|
+
|
|
196
|
+
PENDING = "pending"
|
|
197
|
+
IN_PROGRESS = "in_progress"
|
|
198
|
+
COMPLETED = "completed"
|
|
199
|
+
FAILED = "failed"
|
|
200
|
+
CANCELLED = "cancelled"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class NacosRegistry(A2ARegistry):
|
|
204
|
+
"""Nacos-based registry implementation for A2A protocol.
|
|
205
|
+
|
|
206
|
+
This registry registers A2A agent services to Nacos service
|
|
207
|
+
discovery system. It performs two-step registration:
|
|
208
|
+
1. Publishes agent card to Nacos (agent metadata)
|
|
209
|
+
2. Registers agent endpoint with host/port information (service location)
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
nacos_client_config: Optional Nacos client configuration.
|
|
213
|
+
If None, creates default config from environment variables.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
nacos_client_config: Optional[ClientConfig] = None,
|
|
219
|
+
):
|
|
220
|
+
self._nacos_client_config = nacos_client_config
|
|
221
|
+
self._nacos_ai_service: Optional[NacosAIService] = None
|
|
222
|
+
self._register_task: Optional[asyncio.Task] = None
|
|
223
|
+
self._register_thread: Optional[threading.Thread] = None
|
|
224
|
+
self._registration_status: RegistrationStatus = (
|
|
225
|
+
RegistrationStatus.PENDING
|
|
226
|
+
)
|
|
227
|
+
self._registration_lock = threading.Lock()
|
|
228
|
+
self._shutdown_event = threading.Event()
|
|
229
|
+
|
|
230
|
+
def registry_name(self) -> str:
|
|
231
|
+
"""Get the name of this registry implementation.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Registry name: "nacos"
|
|
235
|
+
"""
|
|
236
|
+
return "nacos"
|
|
237
|
+
|
|
238
|
+
def register(
|
|
239
|
+
self,
|
|
240
|
+
agent_card: AgentCard,
|
|
241
|
+
a2a_transports_properties: Optional[
|
|
242
|
+
List[A2ATransportsProperties]
|
|
243
|
+
] = None,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Register an A2A agent service to Nacos.
|
|
246
|
+
|
|
247
|
+
This method initiates registration asynchronously in the
|
|
248
|
+
background. Exceptions during registration are caught and
|
|
249
|
+
logged, but not raised. Use `get_registration_status()` or
|
|
250
|
+
`wait_for_registration()` to check the result.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
agent_card: The complete A2A agent card generated by runtime
|
|
254
|
+
a2a_transports_properties: List of transport configurations.
|
|
255
|
+
Each transport will be registered separately.
|
|
256
|
+
"""
|
|
257
|
+
if not _NACOS_SDK_AVAILABLE:
|
|
258
|
+
logger.warning(
|
|
259
|
+
"[NacosRegistry] Nacos SDK (nacos-sdk-python) is not "
|
|
260
|
+
"available. Install it with: pip install nacos-sdk-python",
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
self._start_register_task(
|
|
265
|
+
agent_card=agent_card,
|
|
266
|
+
a2a_transports_properties=a2a_transports_properties,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _start_register_task(
|
|
270
|
+
self,
|
|
271
|
+
agent_card: AgentCard,
|
|
272
|
+
a2a_transports_properties: Optional[
|
|
273
|
+
List[A2ATransportsProperties]
|
|
274
|
+
] = None,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Start background Nacos registration task.
|
|
277
|
+
|
|
278
|
+
If there is an active asyncio event loop, schedule the
|
|
279
|
+
registration as a task on that loop. Otherwise, spawn a daemon
|
|
280
|
+
thread and run the registration using asyncio.run so
|
|
281
|
+
registration still occurs in synchronous contexts.
|
|
282
|
+
"""
|
|
283
|
+
with self._registration_lock:
|
|
284
|
+
if self._shutdown_event.is_set():
|
|
285
|
+
logger.info(
|
|
286
|
+
"[NacosRegistry] Shutdown already requested, "
|
|
287
|
+
"skipping registration",
|
|
288
|
+
)
|
|
289
|
+
self._registration_status = RegistrationStatus.CANCELLED
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
if self._registration_status in (
|
|
293
|
+
RegistrationStatus.IN_PROGRESS,
|
|
294
|
+
RegistrationStatus.COMPLETED,
|
|
295
|
+
):
|
|
296
|
+
logger.warning(
|
|
297
|
+
"[NacosRegistry] Registration already in "
|
|
298
|
+
"progress or completed, skipping",
|
|
299
|
+
)
|
|
300
|
+
return
|
|
301
|
+
self._registration_status = RegistrationStatus.PENDING
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
loop = None
|
|
305
|
+
try:
|
|
306
|
+
loop = asyncio.get_running_loop()
|
|
307
|
+
except RuntimeError:
|
|
308
|
+
loop = None
|
|
309
|
+
|
|
310
|
+
if loop is not None:
|
|
311
|
+
with self._registration_lock:
|
|
312
|
+
self._registration_status = RegistrationStatus.IN_PROGRESS
|
|
313
|
+
self._register_task = loop.create_task(
|
|
314
|
+
self._register_to_nacos(
|
|
315
|
+
agent_card=agent_card,
|
|
316
|
+
a2a_transports_properties=a2a_transports_properties,
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
logger.info(
|
|
320
|
+
"[NacosRegistry] Registration task scheduled on "
|
|
321
|
+
"running event loop",
|
|
322
|
+
)
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
def _thread_runner():
|
|
326
|
+
try:
|
|
327
|
+
with self._registration_lock:
|
|
328
|
+
if self._shutdown_event.is_set():
|
|
329
|
+
logger.info(
|
|
330
|
+
"[NacosRegistry] Shutdown requested "
|
|
331
|
+
"before registration started",
|
|
332
|
+
)
|
|
333
|
+
self._registration_status = (
|
|
334
|
+
RegistrationStatus.CANCELLED
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
self._registration_status = (
|
|
338
|
+
RegistrationStatus.IN_PROGRESS
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
asyncio.run(
|
|
342
|
+
self._register_to_nacos(
|
|
343
|
+
agent_card=agent_card,
|
|
344
|
+
a2a_transports_properties=(
|
|
345
|
+
a2a_transports_properties
|
|
346
|
+
),
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
with self._registration_lock:
|
|
350
|
+
if (
|
|
351
|
+
self._registration_status
|
|
352
|
+
== RegistrationStatus.IN_PROGRESS
|
|
353
|
+
):
|
|
354
|
+
self._registration_status = (
|
|
355
|
+
RegistrationStatus.COMPLETED
|
|
356
|
+
)
|
|
357
|
+
except asyncio.CancelledError:
|
|
358
|
+
with self._registration_lock:
|
|
359
|
+
self._registration_status = (
|
|
360
|
+
RegistrationStatus.CANCELLED
|
|
361
|
+
)
|
|
362
|
+
logger.info("[NacosRegistry] Registration cancelled")
|
|
363
|
+
except Exception:
|
|
364
|
+
with self._registration_lock:
|
|
365
|
+
self._registration_status = RegistrationStatus.FAILED
|
|
366
|
+
logger.error(
|
|
367
|
+
"[NacosRegistry] Registration failed in "
|
|
368
|
+
"background thread",
|
|
369
|
+
exc_info=True,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
thread = threading.Thread(
|
|
373
|
+
target=_thread_runner,
|
|
374
|
+
name="nacos-registry-register",
|
|
375
|
+
daemon=True,
|
|
376
|
+
)
|
|
377
|
+
thread.start()
|
|
378
|
+
with self._registration_lock:
|
|
379
|
+
self._register_thread = thread
|
|
380
|
+
logger.info(
|
|
381
|
+
"[NacosRegistry] Registration task started in "
|
|
382
|
+
"background thread",
|
|
383
|
+
)
|
|
384
|
+
except Exception:
|
|
385
|
+
with self._registration_lock:
|
|
386
|
+
self._registration_status = RegistrationStatus.FAILED
|
|
387
|
+
logger.warning(
|
|
388
|
+
"[NacosRegistry] Error starting registration task",
|
|
389
|
+
exc_info=True,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def _get_client_config(self) -> ClientConfig:
|
|
393
|
+
"""Get Nacos client configuration.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
ClientConfig: Nacos client configuration
|
|
397
|
+
"""
|
|
398
|
+
if self._nacos_client_config is not None:
|
|
399
|
+
return self._nacos_client_config
|
|
400
|
+
|
|
401
|
+
settings = get_nacos_settings()
|
|
402
|
+
return _build_nacos_client_config(settings)
|
|
403
|
+
|
|
404
|
+
# pylint: disable=too-many-branches,too-many-statements
|
|
405
|
+
async def _register_to_nacos(
|
|
406
|
+
self,
|
|
407
|
+
agent_card: AgentCard,
|
|
408
|
+
a2a_transports_properties: Optional[
|
|
409
|
+
List[A2ATransportsProperties]
|
|
410
|
+
] = None,
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Register agent to Nacos.
|
|
413
|
+
|
|
414
|
+
Performs two-step registration:
|
|
415
|
+
1. Release agent card to Nacos
|
|
416
|
+
2. Register agent endpoint with host/port information
|
|
417
|
+
|
|
418
|
+
On successful registration, the NacosAIService connection is kept
|
|
419
|
+
alive to maintain heartbeat and health checks. The service will be
|
|
420
|
+
closed in cleanup() during application shutdown.
|
|
421
|
+
|
|
422
|
+
Most exceptions are caught and logged without re-raising to prevent
|
|
423
|
+
background tasks from crashing the host. Only `asyncio.CancelledError`
|
|
424
|
+
is re-raised.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
agent_card: The A2A agent card to register
|
|
428
|
+
a2a_transports_properties: Optional list of transport
|
|
429
|
+
configurations
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
asyncio.CancelledError: If the registration task is cancelled
|
|
433
|
+
"""
|
|
434
|
+
# Check if shutdown was requested
|
|
435
|
+
if self._shutdown_event.is_set():
|
|
436
|
+
with self._registration_lock:
|
|
437
|
+
self._registration_status = RegistrationStatus.CANCELLED
|
|
438
|
+
logger.info(
|
|
439
|
+
"[NacosRegistry] Registration cancelled due to shutdown",
|
|
440
|
+
)
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
client_config = self._get_client_config()
|
|
444
|
+
try:
|
|
445
|
+
self._nacos_ai_service = await NacosAIService.create_ai_service(
|
|
446
|
+
client_config,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Check again after service creation
|
|
450
|
+
if self._shutdown_event.is_set():
|
|
451
|
+
with self._registration_lock:
|
|
452
|
+
self._registration_status = RegistrationStatus.CANCELLED
|
|
453
|
+
logger.info(
|
|
454
|
+
"[NacosRegistry] Registration cancelled after "
|
|
455
|
+
"service creation",
|
|
456
|
+
)
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
# Publish agent card
|
|
460
|
+
await self._nacos_ai_service.release_agent_card(
|
|
461
|
+
ReleaseAgentCardParam(agent_card=agent_card),
|
|
462
|
+
)
|
|
463
|
+
logger.info(
|
|
464
|
+
"[NacosRegistry] Agent card published: agent=%s",
|
|
465
|
+
agent_card.name,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Check again before endpoint registration
|
|
469
|
+
if self._shutdown_event.is_set():
|
|
470
|
+
with self._registration_lock:
|
|
471
|
+
self._registration_status = RegistrationStatus.CANCELLED
|
|
472
|
+
logger.warning(
|
|
473
|
+
"[NacosRegistry] Registration cancelled after "
|
|
474
|
+
"card publish, endpoint may be partially "
|
|
475
|
+
"registered",
|
|
476
|
+
)
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Register agent endpoint
|
|
480
|
+
if a2a_transports_properties:
|
|
481
|
+
for transport in a2a_transports_properties:
|
|
482
|
+
host = transport.host
|
|
483
|
+
port = transport.port
|
|
484
|
+
endpoint_param = RegisterAgentEndpointParam(
|
|
485
|
+
agent_name=agent_card.name,
|
|
486
|
+
version=agent_card.version,
|
|
487
|
+
address=host,
|
|
488
|
+
port=port,
|
|
489
|
+
path=transport.path,
|
|
490
|
+
support_tls=transport.support_tls,
|
|
491
|
+
transport=transport.transport_type,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
await self._nacos_ai_service.register_agent_endpoint(
|
|
495
|
+
endpoint_param,
|
|
496
|
+
)
|
|
497
|
+
logger.info(
|
|
498
|
+
"[NacosRegistry] Agent endpoint registered: "
|
|
499
|
+
"agent=%s, address=%s:%s, path=%s",
|
|
500
|
+
agent_card.name,
|
|
501
|
+
host,
|
|
502
|
+
port,
|
|
503
|
+
transport.path,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
with self._registration_lock:
|
|
507
|
+
if self._registration_status == RegistrationStatus.IN_PROGRESS:
|
|
508
|
+
self._registration_status = RegistrationStatus.COMPLETED
|
|
509
|
+
# Service remains alive to maintain heartbeat and health checks.
|
|
510
|
+
# It will be closed in cleanup() during application shutdown.
|
|
511
|
+
|
|
512
|
+
except asyncio.CancelledError:
|
|
513
|
+
with self._registration_lock:
|
|
514
|
+
self._registration_status = RegistrationStatus.CANCELLED
|
|
515
|
+
logger.info("[NacosRegistry] Registration task cancelled")
|
|
516
|
+
try:
|
|
517
|
+
svc = self._nacos_ai_service
|
|
518
|
+
if svc is not None:
|
|
519
|
+
await svc.shutdown()
|
|
520
|
+
logger.debug(
|
|
521
|
+
"[NacosRegistry] NacosAIService closed due to "
|
|
522
|
+
"cancellation",
|
|
523
|
+
)
|
|
524
|
+
except Exception:
|
|
525
|
+
logger.debug(
|
|
526
|
+
"[NacosRegistry] Error closing NacosAIService on "
|
|
527
|
+
"cancellation",
|
|
528
|
+
exc_info=True,
|
|
529
|
+
)
|
|
530
|
+
raise
|
|
531
|
+
except Exception as e:
|
|
532
|
+
with self._registration_lock:
|
|
533
|
+
self._registration_status = RegistrationStatus.FAILED
|
|
534
|
+
# Log errors but don't re-raise; background tasks should
|
|
535
|
+
# not crash the host
|
|
536
|
+
logger.error(
|
|
537
|
+
"[NacosRegistry] Failed to register agent=%s: %s",
|
|
538
|
+
agent_card.name,
|
|
539
|
+
str(e),
|
|
540
|
+
exc_info=True,
|
|
541
|
+
)
|
|
542
|
+
try:
|
|
543
|
+
svc = self._nacos_ai_service
|
|
544
|
+
if svc is not None:
|
|
545
|
+
await svc.shutdown()
|
|
546
|
+
logger.debug(
|
|
547
|
+
"[NacosRegistry] NacosAIService closed due to "
|
|
548
|
+
"registration failure",
|
|
549
|
+
)
|
|
550
|
+
except Exception:
|
|
551
|
+
logger.debug(
|
|
552
|
+
"[NacosRegistry] Error closing NacosAIService on failure",
|
|
553
|
+
exc_info=True,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
def get_registration_status(self) -> RegistrationStatus:
|
|
557
|
+
"""Get the current registration status.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
RegistrationStatus: Current status of the registration task
|
|
561
|
+
"""
|
|
562
|
+
with self._registration_lock:
|
|
563
|
+
return self._registration_status
|
|
564
|
+
|
|
565
|
+
async def wait_for_registration(
|
|
566
|
+
self,
|
|
567
|
+
timeout: Optional[float] = None,
|
|
568
|
+
) -> RegistrationStatus:
|
|
569
|
+
"""Wait for registration to complete, fail, or be cancelled.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
timeout: Optional timeout in seconds. If None, waits indefinitely.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
RegistrationStatus: Final status of the registration
|
|
576
|
+
"""
|
|
577
|
+
if self._register_task is not None:
|
|
578
|
+
# Task-based registration: wait for the task
|
|
579
|
+
try:
|
|
580
|
+
if timeout is not None:
|
|
581
|
+
await asyncio.wait_for(
|
|
582
|
+
self._register_task,
|
|
583
|
+
timeout=timeout,
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
await self._register_task
|
|
587
|
+
except asyncio.TimeoutError:
|
|
588
|
+
logger.warning(
|
|
589
|
+
"[NacosRegistry] Wait for registration timed "
|
|
590
|
+
"out after %s seconds",
|
|
591
|
+
timeout,
|
|
592
|
+
)
|
|
593
|
+
except (asyncio.CancelledError, Exception):
|
|
594
|
+
pass # Task may have been cancelled or failed
|
|
595
|
+
elif self._register_thread is not None:
|
|
596
|
+
# Thread-based registration: wait for the thread (matching
|
|
597
|
+
# LocalDeployManager pattern)
|
|
598
|
+
self._register_thread.join(timeout=timeout)
|
|
599
|
+
if self._register_thread.is_alive():
|
|
600
|
+
logger.warning(
|
|
601
|
+
"[NacosRegistry] Registration thread did not "
|
|
602
|
+
"complete within timeout",
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return self.get_registration_status()
|
|
606
|
+
|
|
607
|
+
# pylint: disable=too-many-branches,too-many-statements
|
|
608
|
+
async def cleanup(
|
|
609
|
+
self,
|
|
610
|
+
wait_for_completion: bool = True,
|
|
611
|
+
timeout: Optional[float] = 5.0,
|
|
612
|
+
) -> None:
|
|
613
|
+
"""Clean up registration resources and cancel ongoing
|
|
614
|
+
registration if needed.
|
|
615
|
+
|
|
616
|
+
This method should be called during shutdown to ensure proper cleanup.
|
|
617
|
+
It will:
|
|
618
|
+
1. Signal shutdown to prevent new registration attempts
|
|
619
|
+
2. Optionally wait for the registration to complete or timeout
|
|
620
|
+
3. Cancel the asyncio task if running
|
|
621
|
+
4. Wait for background thread if running
|
|
622
|
+
5. Clean up service connections
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
wait_for_completion: If True, wait for registration to complete
|
|
626
|
+
before cancelling. If False, cancel immediately.
|
|
627
|
+
timeout: Maximum time to wait for completion (if
|
|
628
|
+
wait_for_completion=True). Default is 5 seconds.
|
|
629
|
+
"""
|
|
630
|
+
logger.info("[NacosRegistry] Starting cleanup")
|
|
631
|
+
|
|
632
|
+
# Signal shutdown first to prevent new registration attempts
|
|
633
|
+
self._shutdown_event.set()
|
|
634
|
+
|
|
635
|
+
with self._registration_lock:
|
|
636
|
+
current_status = self._registration_status
|
|
637
|
+
|
|
638
|
+
# If registration is in progress or pending, handle cancellation
|
|
639
|
+
if current_status in (
|
|
640
|
+
RegistrationStatus.IN_PROGRESS,
|
|
641
|
+
RegistrationStatus.PENDING,
|
|
642
|
+
):
|
|
643
|
+
if (
|
|
644
|
+
wait_for_completion
|
|
645
|
+
and current_status == RegistrationStatus.IN_PROGRESS
|
|
646
|
+
):
|
|
647
|
+
logger.info(
|
|
648
|
+
"[NacosRegistry] Waiting for registration to "
|
|
649
|
+
"complete (timeout=%s)",
|
|
650
|
+
timeout,
|
|
651
|
+
)
|
|
652
|
+
try:
|
|
653
|
+
await self.wait_for_registration(timeout=timeout)
|
|
654
|
+
current_status = self.get_registration_status()
|
|
655
|
+
if current_status == RegistrationStatus.COMPLETED:
|
|
656
|
+
logger.info(
|
|
657
|
+
"[NacosRegistry] Registration completed "
|
|
658
|
+
"before shutdown",
|
|
659
|
+
)
|
|
660
|
+
elif current_status == RegistrationStatus.FAILED:
|
|
661
|
+
logger.warning(
|
|
662
|
+
"[NacosRegistry] Registration failed "
|
|
663
|
+
"before shutdown",
|
|
664
|
+
)
|
|
665
|
+
elif current_status == RegistrationStatus.CANCELLED:
|
|
666
|
+
logger.info(
|
|
667
|
+
"[NacosRegistry] Registration was cancelled",
|
|
668
|
+
)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
logger.warning(
|
|
671
|
+
"[NacosRegistry] Error waiting for registration: %s",
|
|
672
|
+
e,
|
|
673
|
+
exc_info=True,
|
|
674
|
+
)
|
|
675
|
+
else:
|
|
676
|
+
if current_status == RegistrationStatus.PENDING:
|
|
677
|
+
logger.info(
|
|
678
|
+
"[NacosRegistry] Registration was pending, "
|
|
679
|
+
"marking as cancelled",
|
|
680
|
+
)
|
|
681
|
+
with self._registration_lock:
|
|
682
|
+
self._registration_status = (
|
|
683
|
+
RegistrationStatus.CANCELLED
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
logger.info(
|
|
687
|
+
"[NacosRegistry] Skipping wait, cancelling "
|
|
688
|
+
"immediately",
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# Cancel the task if it's still running (matching
|
|
692
|
+
# env_service pattern)
|
|
693
|
+
if (
|
|
694
|
+
self._register_task is not None
|
|
695
|
+
and not self._register_task.done()
|
|
696
|
+
):
|
|
697
|
+
logger.info("[NacosRegistry] Cancelling registration task")
|
|
698
|
+
self._register_task.cancel()
|
|
699
|
+
try:
|
|
700
|
+
await self._register_task
|
|
701
|
+
except asyncio.CancelledError:
|
|
702
|
+
# Expected when task is cancelled: await on a
|
|
703
|
+
# cancelled task raises CancelledError
|
|
704
|
+
pass
|
|
705
|
+
except Exception as e:
|
|
706
|
+
logger.debug(
|
|
707
|
+
"[NacosRegistry] Error cancelling task: %s",
|
|
708
|
+
e,
|
|
709
|
+
exc_info=True,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Wait for thread if it's still running (matching
|
|
713
|
+
# LocalDeployManager pattern)
|
|
714
|
+
if (
|
|
715
|
+
self._register_thread is not None
|
|
716
|
+
and self._register_thread.is_alive()
|
|
717
|
+
):
|
|
718
|
+
logger.info(
|
|
719
|
+
"[NacosRegistry] Waiting for registration thread "
|
|
720
|
+
"to finish",
|
|
721
|
+
)
|
|
722
|
+
thread_timeout = (
|
|
723
|
+
timeout
|
|
724
|
+
if (
|
|
725
|
+
wait_for_completion
|
|
726
|
+
and current_status == RegistrationStatus.IN_PROGRESS
|
|
727
|
+
)
|
|
728
|
+
else 1.0
|
|
729
|
+
)
|
|
730
|
+
self._register_thread.join(timeout=thread_timeout)
|
|
731
|
+
if self._register_thread.is_alive():
|
|
732
|
+
logger.warning(
|
|
733
|
+
"[NacosRegistry] Registration thread did not "
|
|
734
|
+
"terminate, potential resource leak",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Always clean up service connection, regardless of status
|
|
738
|
+
if self._nacos_ai_service is not None:
|
|
739
|
+
try:
|
|
740
|
+
await self._nacos_ai_service.shutdown()
|
|
741
|
+
except Exception:
|
|
742
|
+
logger.debug(
|
|
743
|
+
"[NacosRegistry] Error closing service during cleanup",
|
|
744
|
+
exc_info=True,
|
|
745
|
+
)
|
|
746
|
+
finally:
|
|
747
|
+
self._nacos_ai_service = None
|
|
748
|
+
|
|
749
|
+
logger.info("[NacosRegistry] Cleanup completed")
|