chore: initial import of standalone agentscope project
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
This commit is contained in:
14
src/agentscope/a2a/__init__.py
Normal file
14
src/agentscope/a2a/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The A2A related modules."""
|
||||
from ._base import AgentCardResolverBase
|
||||
from ._file_resolver import FileAgentCardResolver
|
||||
from ._well_known_resolver import WellKnownAgentCardResolver
|
||||
from ._nacos_resolver import NacosAgentCardResolver
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentCardResolverBase",
|
||||
"FileAgentCardResolver",
|
||||
"WellKnownAgentCardResolver",
|
||||
"NacosAgentCardResolver",
|
||||
]
|
||||
25
src/agentscope/a2a/_base.py
Normal file
25
src/agentscope/a2a/_base.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The A2A agent card resolver base class."""
|
||||
from abc import abstractmethod
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import AgentCard
|
||||
else:
|
||||
AgentCard = "a2a.types.AgentCard"
|
||||
|
||||
|
||||
class AgentCardResolverBase:
|
||||
"""Base class for A2A agent card resolvers, responsible for fetching
|
||||
agent cards from various sources. Implementations must provide the
|
||||
`get_agent_card` method to retrieve the agent card.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_agent_card(self, *args: Any, **kwargs: Any) -> AgentCard:
|
||||
"""Get Agent Card from the configured source.
|
||||
|
||||
Returns:
|
||||
`AgentCard`:
|
||||
The resolved agent card object.
|
||||
"""
|
||||
78
src/agentscope/a2a/_file_resolver.py
Normal file
78
src/agentscope/a2a/_file_resolver.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The JSON file based A2A agent card resolver."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._base import AgentCardResolverBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import AgentCard
|
||||
else:
|
||||
AgentCard = "a2a.types.AgentCard"
|
||||
|
||||
|
||||
class FileAgentCardResolver(AgentCardResolverBase):
|
||||
"""Agent card resolver that loads AgentCard from a JSON file.
|
||||
|
||||
The JSON file should contain an AgentCard object with the following
|
||||
required fields:
|
||||
|
||||
- name (str): The name of the agent
|
||||
- url (str): The URL of the agent
|
||||
- version (str): The version of the agent
|
||||
- capabilities (dict): The capabilities of the agent
|
||||
- default_input_modes (list[str]): Default input modes
|
||||
- default_output_modes (list[str]): Default output modes
|
||||
- skills (list): List of agent skills
|
||||
|
||||
Example JSON file content:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"name": "RemoteAgent",
|
||||
"url": "http://localhost:8000",
|
||||
"description": "A remote A2A agent",
|
||||
"version": "1.0.0",
|
||||
"capabilities": {},
|
||||
"default_input_modes": ["text/plain"],
|
||||
"default_output_modes": ["text/plain"],
|
||||
"skills": []
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: str,
|
||||
) -> None:
|
||||
"""Initialize the FileAgentCardResolver with the path to the JSON file.
|
||||
|
||||
Args:
|
||||
file_path (`str`):
|
||||
The path to the JSON file containing the agent card.
|
||||
"""
|
||||
self._file_path = file_path
|
||||
|
||||
async def get_agent_card(self) -> AgentCard:
|
||||
"""Get the agent card from the JSON file.
|
||||
|
||||
Returns:
|
||||
`AgentCard`:
|
||||
The agent card loaded from the file.
|
||||
"""
|
||||
from a2a.types import AgentCard
|
||||
|
||||
path = Path(self._file_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Agent card file not found: {self._file_path}",
|
||||
)
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"Path is not a file: {self._file_path}")
|
||||
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
agent_json_data = json.load(f)
|
||||
return AgentCard.model_validate(agent_json_data)
|
||||
98
src/agentscope/a2a/_nacos_resolver.py
Normal file
98
src/agentscope/a2a/_nacos_resolver.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The Nacos-based A2A Agent Card resolver."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._base import AgentCardResolverBase
|
||||
from .._logging import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import AgentCard
|
||||
from v2.nacos.common.client_config import ClientConfig
|
||||
else:
|
||||
AgentCard = "a2a.types.AgentCard"
|
||||
ClientConfig = "v2.nacos.common.client_config.ClientConfig"
|
||||
|
||||
|
||||
class NacosAgentCardResolver(AgentCardResolverBase):
|
||||
"""Nacos-based A2A Agent Card resolver.
|
||||
|
||||
Nacos is a dynamic service discovery, configuration and service
|
||||
management platform for building cloud native applications. This resolver
|
||||
fetches the agent card from a Nacos server and subscribes to updates.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
remote_agent_name: str,
|
||||
nacos_client_config: ClientConfig,
|
||||
version: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the nacos agent card resolver.
|
||||
|
||||
Args:
|
||||
remote_agent_name (`str`):
|
||||
Name of the remote agent in Nacos.
|
||||
nacos_client_config (`ClientConfig | None`, optional):
|
||||
Nacos client configuration, where a `server_addresses`
|
||||
parameter is required.
|
||||
version (`str | None`, optional):
|
||||
Version of the agent card to fetch. If None, fetches the
|
||||
latest version. This version is also used when subscribing
|
||||
to agent card updates.
|
||||
Defaults to None (latest version).
|
||||
"""
|
||||
if not remote_agent_name:
|
||||
raise ValueError(
|
||||
"The remote_agent_name cannot be empty.",
|
||||
)
|
||||
|
||||
if not nacos_client_config:
|
||||
raise ValueError(
|
||||
"The nacos_client_config cannot be None.",
|
||||
)
|
||||
|
||||
self._nacos_client_config = nacos_client_config
|
||||
self._remote_agent_name = remote_agent_name
|
||||
self._version = version
|
||||
|
||||
async def get_agent_card(self) -> AgentCard:
|
||||
"""Get agent card from Nacos with lazy initialization.
|
||||
|
||||
Returns:
|
||||
`AgentCard`:
|
||||
The resolved agent card from Nacos.
|
||||
"""
|
||||
try:
|
||||
from v2.nacos.ai.model.ai_param import GetAgentCardParam
|
||||
from v2.nacos.ai.nacos_ai_service import NacosAIService
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Please install the nacos sdk by running `pip install "
|
||||
"nacos-sdk-python>=3.0.0` first.",
|
||||
) from e
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = await NacosAIService.create_ai_service(
|
||||
self._nacos_client_config,
|
||||
)
|
||||
|
||||
await client.start()
|
||||
return await client.get_agent_card(
|
||||
GetAgentCardParam(
|
||||
agent_name=self._remote_agent_name,
|
||||
version=self._version,
|
||||
),
|
||||
)
|
||||
|
||||
finally:
|
||||
if client:
|
||||
# Close the Nacos client to free resources
|
||||
try:
|
||||
await client.shutdown()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to shutdown Nacos client: %s",
|
||||
str(e),
|
||||
)
|
||||
90
src/agentscope/a2a/_well_known_resolver.py
Normal file
90
src/agentscope/a2a/_well_known_resolver.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The A2A well-known agent card resolver."""
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ._base import AgentCardResolverBase
|
||||
from .._logging import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import AgentCard
|
||||
else:
|
||||
AgentCard = "a2a.types.AgentCard"
|
||||
|
||||
|
||||
class WellKnownAgentCardResolver(AgentCardResolverBase):
|
||||
"""Agent card resolver that loads AgentCard from a well-known URL."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
agent_card_path: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the WellKnownAgentCardResolver.
|
||||
|
||||
Args:
|
||||
base_url (`str`):
|
||||
The base URL to resolve the agent card from.
|
||||
agent_card_path (`str | None`, optional):
|
||||
The path to the agent card relative to the base URL.
|
||||
Defaults to AGENT_CARD_WELL_KNOWN_PATH from a2a.utils.
|
||||
"""
|
||||
self._base_url = base_url
|
||||
self._agent_card_path = agent_card_path
|
||||
|
||||
async def get_agent_card(self) -> AgentCard:
|
||||
"""Get the agent card from the well-known URL.
|
||||
|
||||
Returns:
|
||||
`AgentCard`:
|
||||
The agent card loaded from the URL.
|
||||
"""
|
||||
import httpx
|
||||
from a2a.client import A2ACardResolver
|
||||
from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH
|
||||
|
||||
try:
|
||||
parsed_url = urlparse(self._base_url)
|
||||
if not parsed_url.scheme or not parsed_url.netloc:
|
||||
logger.error(
|
||||
"[%s] Invalid URL format: %s",
|
||||
self.__class__.__name__,
|
||||
self._base_url,
|
||||
)
|
||||
raise ValueError(
|
||||
f"Invalid URL format: {self._base_url}",
|
||||
)
|
||||
|
||||
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
relative_card_path = parsed_url.path
|
||||
|
||||
# Use default path if not specified
|
||||
agent_card_path = (
|
||||
self._agent_card_path
|
||||
if self._agent_card_path is not None
|
||||
else AGENT_CARD_WELL_KNOWN_PATH
|
||||
)
|
||||
|
||||
# Use async context manager to ensure proper cleanup
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(timeout=600),
|
||||
) as _http_client:
|
||||
resolver = A2ACardResolver(
|
||||
httpx_client=_http_client,
|
||||
base_url=base_url,
|
||||
agent_card_path=agent_card_path,
|
||||
)
|
||||
return await resolver.get_agent_card(
|
||||
relative_card_path=relative_card_path,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"[%s] Failed to resolve agent card from URL %s: %s",
|
||||
self.__class__.__name__,
|
||||
self._base_url,
|
||||
e,
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Failed to resolve AgentCard from URL "
|
||||
f"{self._base_url}: {e}",
|
||||
) from e
|
||||
Reference in New Issue
Block a user