agentscope-runtime 1.0.2__py3-none-any.whl → 1.0.3__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.
Files changed (25) hide show
  1. agentscope_runtime/cli/commands/deploy.py +12 -0
  2. agentscope_runtime/common/collections/redis_mapping.py +4 -1
  3. agentscope_runtime/engine/app/agent_app.py +48 -5
  4. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +56 -1
  5. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +449 -41
  6. agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +273 -0
  7. agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +640 -0
  8. agentscope_runtime/engine/deployers/kubernetes_deployer.py +3 -0
  9. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +8 -2
  10. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +5 -0
  11. agentscope_runtime/engine/deployers/utils/net_utils.py +65 -0
  12. agentscope_runtime/engine/runner.py +5 -3
  13. agentscope_runtime/engine/schemas/exception.py +24 -0
  14. agentscope_runtime/engine/services/agent_state/redis_state_service.py +61 -8
  15. agentscope_runtime/engine/services/agent_state/state_service_factory.py +2 -5
  16. agentscope_runtime/engine/services/memory/redis_memory_service.py +129 -25
  17. agentscope_runtime/engine/services/session_history/redis_session_history_service.py +160 -34
  18. agentscope_runtime/sandbox/build.py +50 -57
  19. agentscope_runtime/version.py +1 -1
  20. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.3.dist-info}/METADATA +9 -3
  21. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.3.dist-info}/RECORD +25 -22
  22. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.3.dist-info}/WHEEL +0 -0
  23. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.3.dist-info}/entry_points.txt +0 -0
  24. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.3.dist-info}/licenses/LICENSE +0 -0
  25. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,79 +1,487 @@
1
1
  # -*- coding: utf-8 -*-
2
- import posixpath
3
- from typing import Callable
2
+ """
3
+ A2A Protocol Adapter for FastAPI
4
+
5
+ This module provides the default A2A (Agent-to-Agent) protocol adapter
6
+ implementation for FastAPI applications. It handles agent card configuration,
7
+ wellknown endpoint setup, and task management.
8
+ """
9
+ import os
10
+ import logging
11
+ from typing import Any, Callable, Dict, List, Optional, Union
12
+ from urllib.parse import urljoin
4
13
 
5
14
  from a2a.server.apps import A2AFastAPIApplication
6
15
  from a2a.server.request_handlers import DefaultRequestHandler
7
16
  from a2a.server.tasks import InMemoryTaskStore
8
- from a2a.types import AgentCard, AgentCapabilities, AgentSkill
17
+ from a2a.types import (
18
+ AgentCapabilities,
19
+ AgentCard,
20
+ AgentSkill,
21
+ )
22
+ from fastapi import FastAPI
23
+ from pydantic import ConfigDict, BaseModel, field_validator
24
+
25
+ from agentscope_runtime.engine.deployers.utils.net_utils import (
26
+ get_first_non_loopback_ip,
27
+ )
28
+ from agentscope_runtime.version import __version__ as runtime_version
9
29
 
10
30
  from .a2a_agent_adapter import A2AExecutor
31
+ from .a2a_registry import (
32
+ A2ARegistry,
33
+ A2ATransportsProperties,
34
+ create_registry_from_env,
35
+ )
36
+
37
+ # NOTE: Do NOT import NacosRegistry at module import time to avoid
38
+ # forcing an optional dependency on environments that don't have nacos
39
+ # SDK installed. Registry is optional: users must explicitly provide a
40
+ # registry instance if needed.
41
+ # from .nacos_a2a_registry import NacosRegistry
11
42
  from ..protocol_adapter import ProtocolAdapter
12
43
 
44
+ logger = logging.getLogger(__name__)
45
+
13
46
  A2A_JSON_RPC_URL = "/a2a"
47
+ DEFAULT_WELLKNOWN_PATH = "/.wellknown/agent-card.json"
48
+ DEFAULT_TASK_TIMEOUT = 60
49
+ DEFAULT_TASK_EVENT_TIMEOUT = 10
50
+ DEFAULT_TRANSPORT = "JSONRPC"
51
+ DEFAULT_INPUT_OUTPUT_MODES = ["text"]
52
+ PORT = int(os.getenv("PORT", "8080"))
53
+ AGENT_VERSION = "1.0.0"
54
+
55
+
56
+ def extract_a2a_config(
57
+ a2a_config: Optional["AgentCardWithRuntimeConfig"] = None,
58
+ ) -> "AgentCardWithRuntimeConfig":
59
+ """Normalize a2a_config to AgentCardWithRuntimeConfig object.
60
+
61
+ Ensures a non-null ``AgentCardWithRuntimeConfig`` instance and sets up
62
+ environment-based registry fallback if registry is not provided.
63
+
64
+ Args:
65
+ a2a_config: Optional AgentCardWithRuntimeConfig instance.
66
+
67
+ Returns:
68
+ Normalized AgentCardWithRuntimeConfig object.
69
+ """
70
+ if a2a_config is None:
71
+ a2a_config = AgentCardWithRuntimeConfig()
72
+
73
+ # Fallback to environment registry if not provided
74
+ if a2a_config.registry is None:
75
+ env_registry = create_registry_from_env()
76
+ if env_registry is not None:
77
+ a2a_config.registry = env_registry
78
+ logger.debug("[A2A] Using registry from environment variables")
79
+
80
+ return a2a_config
81
+
82
+
83
+ class AgentCardWithRuntimeConfig(BaseModel):
84
+ """Runtime configuration wrapper for AgentCard.
85
+
86
+ Combines AgentCard (protocol fields) with runtime-specific settings
87
+ (host, port, registry, timeouts, etc.) in a single configuration object.
88
+
89
+ Attributes:
90
+ agent_card: AgentCard object or dict containing protocol fields
91
+ (name, description, url, version, skills, etc.)
92
+ host: Host address for A2A endpoints (default: auto-detected)
93
+ port: Port for A2A endpoints (default: from PORT env var or 8080)
94
+ registry: List of A2A registry instances for service discovery
95
+ task_timeout: Task completion timeout in seconds (default: 60)
96
+ task_event_timeout: Task event timeout in seconds (default: 10)
97
+ wellknown_path: Wellknown endpoint path
98
+ (default: /.wellknown/agent-card.json)
99
+ """
100
+
101
+ agent_card: Optional[Union[AgentCard, Dict[str, Any]]] = None
102
+ host: Optional[str] = None
103
+ port: int = PORT
104
+ registry: Optional[Union[A2ARegistry, List[A2ARegistry]]] = None
105
+ task_timeout: Optional[int] = DEFAULT_TASK_TIMEOUT
106
+ task_event_timeout: Optional[int] = DEFAULT_TASK_EVENT_TIMEOUT
107
+ wellknown_path: Optional[str] = DEFAULT_WELLKNOWN_PATH
108
+
109
+ @field_validator("registry", mode="before")
110
+ @classmethod
111
+ def normalize_registry(cls, v):
112
+ """Normalize registry to list format."""
113
+ if v is None:
114
+ return None
115
+ if isinstance(v, list):
116
+ return v
117
+ # Single registry instance -> convert to list
118
+ return [v]
119
+
120
+ model_config = ConfigDict(
121
+ arbitrary_types_allowed=True,
122
+ extra="allow",
123
+ )
14
124
 
15
125
 
16
126
  class A2AFastAPIDefaultAdapter(ProtocolAdapter):
17
- def __init__(self, agent_name, agent_description, **kwargs):
127
+ """Default A2A protocol adapter for FastAPI applications.
128
+
129
+ Provides comprehensive configuration options for A2A protocol including
130
+ agent card settings, task timeouts, wellknown endpoints, and transport
131
+ configurations. All configuration items have sensible defaults but can
132
+ be overridden by users.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ agent_name: str,
138
+ agent_description: str,
139
+ a2a_config: Optional[AgentCardWithRuntimeConfig] = None,
140
+ **kwargs: Any,
141
+ ) -> None:
142
+ """Initialize A2A protocol adapter.
143
+
144
+ Args:
145
+ agent_name: Agent name
146
+ (fallback if not in a2a_config.agent_card)
147
+ agent_description: Agent description
148
+ (fallback if not in a2a_config.agent_card)
149
+ a2a_config: Runtime configuration with AgentCard and runtime
150
+ settings
151
+ **kwargs: Additional arguments for parent class
152
+ """
18
153
  super().__init__(**kwargs)
19
- self._agent_name = agent_name
20
- self._agent_description = agent_description
21
154
  self._json_rpc_path = kwargs.get("json_rpc_path", A2A_JSON_RPC_URL)
22
- self._base_url = kwargs.get("base_url")
23
155
 
24
- def add_endpoint(self, app, func: Callable, **kwargs):
156
+ if a2a_config is None:
157
+ a2a_config = AgentCardWithRuntimeConfig()
158
+ self._a2a_config = a2a_config
159
+
160
+ # Extract name/description from agent_card, fallback to parameters
161
+ agent_card_name = None
162
+ agent_card_description = None
163
+ if a2a_config.agent_card is not None:
164
+ if isinstance(a2a_config.agent_card, dict):
165
+ agent_card_name = a2a_config.agent_card.get("name")
166
+ agent_card_description = a2a_config.agent_card.get(
167
+ "description",
168
+ )
169
+ elif isinstance(a2a_config.agent_card, AgentCard):
170
+ agent_card_name = getattr(a2a_config.agent_card, "name", None)
171
+ agent_card_description = getattr(
172
+ a2a_config.agent_card,
173
+ "description",
174
+ None,
175
+ )
176
+
177
+ self._agent_name = (
178
+ agent_card_name if agent_card_name is not None else agent_name
179
+ )
180
+ self._agent_description = (
181
+ agent_card_description
182
+ if agent_card_description is not None
183
+ else agent_description
184
+ )
185
+ self._host = a2a_config.host or get_first_non_loopback_ip()
186
+ self._port = a2a_config.port
187
+
188
+ # Normalize registry to list
189
+ registry = a2a_config.registry
190
+ if registry is None:
191
+ self._registry: List[A2ARegistry] = []
192
+ elif isinstance(registry, A2ARegistry):
193
+ self._registry = [registry]
194
+ elif isinstance(registry, list):
195
+ if not all(isinstance(r, A2ARegistry) for r in registry):
196
+ error_msg = (
197
+ "[A2A] Invalid registry list: all items must be "
198
+ "A2ARegistry instances"
199
+ )
200
+ logger.error(error_msg)
201
+ raise TypeError(error_msg)
202
+ self._registry = registry
203
+
204
+ self._task_timeout = a2a_config.task_timeout or DEFAULT_TASK_TIMEOUT
205
+ self._task_event_timeout = (
206
+ a2a_config.task_event_timeout or DEFAULT_TASK_EVENT_TIMEOUT
207
+ )
208
+ self._wellknown_path = (
209
+ a2a_config.wellknown_path or DEFAULT_WELLKNOWN_PATH
210
+ )
211
+
212
+ def add_endpoint(
213
+ self,
214
+ app: FastAPI,
215
+ func: Callable,
216
+ **kwargs: Any,
217
+ ) -> None:
218
+ """Add A2A protocol endpoints to FastAPI application.
219
+
220
+ Args:
221
+ app: FastAPI application instance
222
+ func: Agent execution function
223
+ **kwargs: Additional arguments for registry registration
224
+ """
25
225
  request_handler = DefaultRequestHandler(
26
226
  agent_executor=A2AExecutor(func=func),
27
227
  task_store=InMemoryTaskStore(),
28
228
  )
29
229
 
30
- agent_card = self.get_agent_card(
31
- agent_name=self._agent_name,
32
- agent_description=self._agent_description,
33
- )
230
+ agent_card = self.get_agent_card(app=app)
34
231
 
35
232
  server = A2AFastAPIApplication(
36
233
  agent_card=agent_card,
37
234
  http_handler=request_handler,
38
235
  )
39
236
 
40
- server.add_routes_to_app(app, rpc_url=self._json_rpc_path)
237
+ server.add_routes_to_app(
238
+ app,
239
+ rpc_url=self._json_rpc_path,
240
+ agent_card_url=self._wellknown_path,
241
+ )
242
+
243
+ if self._registry:
244
+ self._register_with_all_registries(
245
+ agent_card=agent_card,
246
+ app=app,
247
+ )
248
+
249
+ def _register_with_all_registries(
250
+ self,
251
+ agent_card: AgentCard,
252
+ app: FastAPI,
253
+ ) -> None:
254
+ """Register agent with all configured registry instances.
255
+
256
+ Registration failures are logged but do not block startup.
41
257
 
42
- def _get_json_rpc_url(self) -> str:
43
- base = self._base_url or "http://127.0.0.1:8000"
44
- return posixpath.join(
45
- base.rstrip("/"),
258
+ Args:
259
+ agent_card: The generated AgentCard
260
+ app: FastAPI application instance
261
+ """
262
+ a2a_transports_properties = self._build_a2a_transports_properties(
263
+ app=app,
264
+ )
265
+
266
+ for registry in self._registry:
267
+ registry_name = registry.registry_name()
268
+ try:
269
+ logger.info(
270
+ "[A2A] Registering with registry: %s",
271
+ registry_name,
272
+ )
273
+ registry.register(
274
+ agent_card=agent_card,
275
+ a2a_transports_properties=a2a_transports_properties,
276
+ )
277
+ logger.info(
278
+ "[A2A] Successfully registered with registry: %s",
279
+ registry_name,
280
+ )
281
+ except Exception as e:
282
+ logger.warning(
283
+ "[A2A] Failed to register with registry %s: %s. "
284
+ "This will not block runtime startup.",
285
+ registry_name,
286
+ str(e),
287
+ exc_info=True,
288
+ )
289
+
290
+ def _build_a2a_transports_properties(
291
+ self,
292
+ app: FastAPI,
293
+ ) -> List[A2ATransportsProperties]:
294
+ """Build A2ATransportsProperties from runtime configuration.
295
+
296
+ Args:
297
+ app: FastAPI application instance
298
+
299
+ Returns:
300
+ List of A2ATransportsProperties instances
301
+ """
302
+ transports_list = []
303
+
304
+ path = getattr(app, "root_path", "")
305
+ json_rpc = urljoin(
306
+ path.rstrip("/") + "/",
46
307
  self._json_rpc_path.lstrip("/"),
47
308
  )
48
309
 
310
+ default_transport = A2ATransportsProperties(
311
+ host=self._host,
312
+ port=self._port,
313
+ path=json_rpc,
314
+ support_tls=False,
315
+ extra={},
316
+ transport_type=DEFAULT_TRANSPORT,
317
+ )
318
+ transports_list.append(default_transport)
319
+
320
+ return transports_list
321
+
322
+ def _get_agent_card_field(
323
+ self,
324
+ field_name: str,
325
+ default: Any = None,
326
+ ) -> Any:
327
+ """Extract field from agent_card (dict or AgentCard object).
328
+
329
+ Args:
330
+ field_name: Field name to retrieve
331
+ default: Default value if not found
332
+
333
+ Returns:
334
+ Field value or default
335
+ """
336
+ agent_card = self._a2a_config.agent_card
337
+ if agent_card is None:
338
+ return default
339
+
340
+ if isinstance(agent_card, dict):
341
+ return agent_card.get(field_name, default)
342
+ else:
343
+ # AgentCard object
344
+ return getattr(agent_card, field_name, default)
345
+
49
346
  def get_agent_card(
50
347
  self,
51
- agent_name: str,
52
- agent_description: str,
348
+ app: Optional[FastAPI] = None, # pylint: disable=unused-argument
53
349
  ) -> AgentCard:
54
- capabilities = AgentCapabilities(
55
- streaming=False,
56
- push_notifications=False,
350
+ """Build AgentCard from configuration.
351
+
352
+ Constructs AgentCard from agent_card field (dict or AgentCard),
353
+ filling missing fields with defaults and computed values.
354
+
355
+ Args:
356
+ app: FastAPI app instance (for URL generation)
357
+
358
+ Returns:
359
+ Configured AgentCard instance
360
+ """
361
+
362
+ # Generate URL if not provided
363
+ url = self._get_agent_card_field("url")
364
+ if url is None:
365
+ path = getattr(app, "root_path", "")
366
+ json_rpc = urljoin(
367
+ path.rstrip("/") + "/",
368
+ self._json_rpc_path.lstrip("/"),
369
+ ).lstrip("/")
370
+ base_url = (
371
+ f"{self._host}:{self._port}"
372
+ if self._host.startswith(("http://", "https://"))
373
+ else f"http://{self._host}:{self._port}"
374
+ )
375
+ url = f"{base_url}/{json_rpc}"
376
+
377
+ # Initialize from agent_card
378
+ card_kwargs = {}
379
+
380
+ # Set required fields
381
+ card_kwargs["name"] = self._get_agent_card_field(
382
+ "name",
383
+ self._agent_name,
57
384
  )
58
- skill = AgentSkill(
59
- id="dialog",
60
- name="Natural Language Dialog Skill",
61
- description="Enables natural language conversation and dialogue "
62
- "with users",
63
- tags=["natural language", "dialog", "conversation"],
64
- examples=[
65
- "Hello, how are you?",
66
- "Can you help me with something?",
67
- ],
385
+ card_kwargs["description"] = self._get_agent_card_field(
386
+ "description",
387
+ self._agent_description,
388
+ )
389
+ card_kwargs["url"] = url
390
+ card_kwargs["version"] = self._get_agent_card_field(
391
+ "version",
392
+ AGENT_VERSION,
68
393
  )
69
394
 
70
- return AgentCard(
71
- capabilities=capabilities,
72
- skills=[skill],
73
- name=agent_name,
74
- description=agent_description,
75
- default_input_modes=["text"],
76
- default_output_modes=["text"],
77
- url=self._get_json_rpc_url(),
78
- version="1.0.0",
395
+ # Set defaults for required fields
396
+ card_kwargs["preferred_transport"] = self._get_agent_card_field(
397
+ "preferred_transport",
398
+ DEFAULT_TRANSPORT,
399
+ )
400
+ card_kwargs["additional_interfaces"] = self._get_agent_card_field(
401
+ "additional_interfaces",
402
+ [],
403
+ )
404
+ card_kwargs["default_input_modes"] = self._get_agent_card_field(
405
+ "default_input_modes",
406
+ DEFAULT_INPUT_OUTPUT_MODES,
407
+ )
408
+ card_kwargs["default_output_modes"] = self._get_agent_card_field(
409
+ "default_output_modes",
410
+ DEFAULT_INPUT_OUTPUT_MODES,
79
411
  )
412
+ card_kwargs["skills"] = self._get_agent_card_field(
413
+ "skills",
414
+ [
415
+ AgentSkill(
416
+ id="dialog",
417
+ name="Natural Language Dialog Skill",
418
+ description=(
419
+ "Enables natural language conversation and dialogue "
420
+ "with users"
421
+ ),
422
+ tags=["natural language", "dialog", "conversation"],
423
+ examples=[
424
+ "Hello, how are you?",
425
+ "Can you help me with something?",
426
+ ],
427
+ ),
428
+ ],
429
+ )
430
+ # Runtime-managed AgentCard fields: user values are ignored
431
+ if self._get_agent_card_field("capabilities") is not None:
432
+ logger.warning(
433
+ "[A2A] Ignoring user-provided AgentCard.capabilities; "
434
+ "runtime controls this field.",
435
+ )
436
+ card_kwargs["capabilities"] = AgentCapabilities(
437
+ streaming=False,
438
+ push_notifications=False,
439
+ state_transition_history=False,
440
+ )
441
+
442
+ if self._get_agent_card_field("protocol_version") is not None:
443
+ logger.warning(
444
+ "[A2A] Ignoring user-provided AgentCard.protocol_version; "
445
+ "runtime controls this field.",
446
+ )
447
+
448
+ if (
449
+ self._get_agent_card_field(
450
+ "supports_authenticated_extended_card",
451
+ )
452
+ is not None
453
+ ):
454
+ logger.warning(
455
+ "[A2A] Ignoring user-provided "
456
+ "AgentCard.supports_authenticated_extended_card; "
457
+ "runtime controls this field.",
458
+ )
459
+
460
+ if self._get_agent_card_field("signatures") is not None:
461
+ logger.warning(
462
+ "[A2A] Ignoring user-provided AgentCard.signatures; "
463
+ "runtime controls this field.",
464
+ )
465
+
466
+ # Add optional fields
467
+ for field in [
468
+ "provider",
469
+ "documentation_url",
470
+ "icon_url",
471
+ "security_schemes",
472
+ "security",
473
+ ]:
474
+ value = self._get_agent_card_field(field)
475
+ if value is None:
476
+ continue
477
+ # Backward compatibility: allow simple string provider and map it
478
+ # to AgentProvider.organization
479
+ if field == "provider" and isinstance(value, str):
480
+ card_kwargs[field] = {
481
+ "organization": value,
482
+ "url": url,
483
+ }
484
+ else:
485
+ card_kwargs[field] = value
486
+
487
+ return AgentCard(**card_kwargs)