From c102706146862bad6908c6f9c417e3122f4fc4f3 Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:53:45 -0700 Subject: [PATCH] Python: basic python a2a support (#906) * basic python a2a support * fixes * small fixes --------- Co-authored-by: Eric Zhu --- python/packages/a2a/LICENSE | 21 + python/packages/a2a/README.md | 21 + .../a2a/agent_framework_a2a/__init__.py | 15 + .../a2a/agent_framework_a2a/_agent.py | 383 ++++++++++++++ python/packages/a2a/pyproject.toml | 89 ++++ python/packages/a2a/tests/test_a2a_agent.py | 498 ++++++++++++++++++ .../main/agent_framework/a2a/__init__.py | 24 + .../main/agent_framework/a2a/__init__.pyi | 5 + python/packages/main/pyproject.toml | 7 +- python/pyproject.toml | 3 + .../getting_started/agents/a2a/README.md | 31 ++ .../agents/a2a/agent_with_a2a.py | 75 +++ python/uv.lock | 126 ++++- 13 files changed, 1296 insertions(+), 2 deletions(-) create mode 100644 python/packages/a2a/LICENSE create mode 100644 python/packages/a2a/README.md create mode 100644 python/packages/a2a/agent_framework_a2a/__init__.py create mode 100644 python/packages/a2a/agent_framework_a2a/_agent.py create mode 100644 python/packages/a2a/pyproject.toml create mode 100644 python/packages/a2a/tests/test_a2a_agent.py create mode 100644 python/packages/main/agent_framework/a2a/__init__.py create mode 100644 python/packages/main/agent_framework/a2a/__init__.pyi create mode 100644 python/samples/getting_started/agents/a2a/README.md create mode 100644 python/samples/getting_started/agents/a2a/agent_with_a2a.py diff --git a/python/packages/a2a/LICENSE b/python/packages/a2a/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/a2a/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/a2a/README.md b/python/packages/a2a/README.md new file mode 100644 index 0000000000..c59b9c8b79 --- /dev/null +++ b/python/packages/a2a/README.md @@ -0,0 +1,21 @@ +# Get Started with Microsoft Agent Framework A2A + +Please install this package as the extra for `agent-framework`: + +```bash +pip install agent-framework[a2a] +``` + +## A2A Agent Integration + +The A2A agent integration enables communication with remote A2A-compliant agents using the standardized A2A protocol. This allows your Agent Framework applications to connect to agents running on different platforms, languages, or services. + +### Basic Usage Example + +See the [A2A agent examples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/agents/a2a/) which demonstrate: + +- Connecting to remote A2A agents +- Sending messages and receiving responses +- Handling different content types (text, files, data) +- Streaming responses and real-time interaction + diff --git a/python/packages/a2a/agent_framework_a2a/__init__.py b/python/packages/a2a/agent_framework_a2a/__init__.py new file mode 100644 index 0000000000..ab4deb98ce --- /dev/null +++ b/python/packages/a2a/agent_framework_a2a/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib.metadata + +from ._agent import A2AAgent + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +__all__ = [ + "A2AAgent", + "__version__", +] diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py new file mode 100644 index 0000000000..be3ed60990 --- /dev/null +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -0,0 +1,383 @@ +# Copyright (c) Microsoft. All rights reserved. + +import base64 +import json +import re +import uuid +from collections.abc import AsyncIterable, Sequence +from typing import Any + +import httpx +from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card +from a2a.types import ( + AgentCard, + Artifact, + FilePart, + FileWithBytes, + FileWithUri, + Message, + Task, + TaskState, + TextPart, + TransportProtocol, +) +from a2a.types import Message as A2AMessage +from a2a.types import Part as A2APart +from a2a.types import Role as A2ARole +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + AgentThread, + BaseAgent, + ChatMessage, + Contents, + DataContent, + Role, + TextContent, + UriContent, + prepend_agent_framework_to_user_agent, +) + +__all__ = ["A2AAgent"] + +URI_PATTERN = re.compile(r"^data:(?P[^;]+);base64,(?P[A-Za-z0-9+/=]+)$") +TERMINAL_TASK_STATES = [ + TaskState.completed, + TaskState.failed, + TaskState.canceled, + TaskState.rejected, +] + + +def _get_uri_data(uri: str) -> str: + match = URI_PATTERN.match(uri) + if not match: + raise ValueError(f"Invalid data URI format: {uri}") + + return match.group("base64_data") + + +class A2AAgent(BaseAgent): + """Agent-to-Agent (A2A) protocol implementation. + + Wraps an A2A Client to connect the Agent Framework with external A2A-compliant agents + via HTTP/JSON-RPC. Converts framework ChatMessages to A2A Messages on send, and converts + A2A responses (Messages/Tasks) back to framework types. Inherits BaseAgent capabilities + while managing the underlying A2A protocol communication. + + Can be initialized with a URL, AgentCard, or existing A2A Client instance. + """ + + client: Client + _http_client: httpx.AsyncClient | None = None + + def __init__( + self, + *, + name: str | None = None, + id: str | None = None, + description: str | None = None, + agent_card: AgentCard | None = None, + url: str | None = None, + client: Client | None = None, + http_client: httpx.AsyncClient | None = None, + ) -> None: + """Initialize the A2AAgent. + + Args: + name: The name of the agent. + id: The unique identifier for the agent, will be created automatically if not provided. + description: A brief description of the agent's purpose. + agent_card: The agent card for the agent. + url: The URL for the A2A server. + client: The A2A client for the agent. + http_client: Optional httpx.AsyncClient to use. + """ + if client is None: + if agent_card is None: + if url is None: + raise ValueError("Either agent_card or url must be provided") + # Create minimal agent card from URL + agent_card = minimal_agent_card(url, [TransportProtocol.jsonrpc]) + + # Create or use provided httpx client + if http_client is None: + timeout = httpx.Timeout( + connect=10.0, # 10 seconds to establish connection + read=60.0, # 60 seconds to read response (A2A operations can take time) + write=10.0, # 10 seconds to send request + pool=5.0, # 5 seconds to get connection from pool + ) + headers = prepend_agent_framework_to_user_agent() + http_client = httpx.AsyncClient(timeout=timeout, headers=headers) + self._http_client = http_client # Store for cleanup + + # Create A2A client using factory + config = ClientConfig( + httpx_client=http_client, + supported_transports=[TransportProtocol.jsonrpc], + ) + factory = ClientFactory(config) + client = factory.create(agent_card) + + args: dict[str, Any] = {"client": client} + if name: + args["name"] = name + if id: + args["id"] = id + if description: + args["description"] = description + super().__init__(**args) + + async def __aenter__(self) -> "A2AAgent": + """Async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: Any, + ) -> None: + """Async context manager exit with httpx client cleanup.""" + # Close our httpx client if we created it + if self._http_client is not None: + await self._http_client.aclose() + + async def run( + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AgentRunResponse: + """Get a response from the agent. + + This method returns the final result of the agent's execution + as a single AgentRunResponse object. The caller is blocked until + the final result is available. + + Args: + messages: The message(s) to send to the agent. + thread: The conversation thread associated with the message(s). + kwargs: Additional keyword arguments. + + Returns: + An agent response item. + """ + # Collect all updates and use framework to consolidate updates into response + updates = [update async for update in self.run_stream(messages, thread=thread, **kwargs)] + return AgentRunResponse.from_agent_run_response_updates(updates) + + async def run_stream( + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentRunResponseUpdate]: + """Run the agent as a stream. + + This method will return the intermediate steps and final results of the + agent's execution as a stream of AgentRunResponseUpdate objects to the caller. + + Args: + messages: The message(s) to send to the agent. + thread: The conversation thread associated with the message(s). + kwargs: Additional keyword arguments. + + Yields: + An agent response item. + """ + messages = self._normalize_messages(messages) + a2a_message = self._chat_message_to_a2a_message(messages[-1]) + + response_stream = self.client.send_message(a2a_message) + + async for item in response_stream: + if isinstance(item, Message): + # Process A2A Message + contents = self._a2a_parts_to_contents(item.parts) + yield AgentRunResponseUpdate( + contents=contents, + role=Role.ASSISTANT if item.role == A2ARole.agent else Role.USER, + response_id=str(getattr(item, "message_id", uuid.uuid4())), + raw_representation=item, + ) + elif isinstance(item, tuple) and len(item) == 2: # ClientEvent = (Task, UpdateEvent) + task, _update_event = item + if isinstance(task, Task) and task.status.state in TERMINAL_TASK_STATES: + # Convert Task artifacts to ChatMessages and yield as separate updates + task_messages = self._task_to_chat_messages(task) + if task_messages: + for message in task_messages: + # Use the artifact's ID from raw_representation as message_id for unique identification + artifact_id = getattr(message.raw_representation, "artifact_id", None) + yield AgentRunResponseUpdate( + contents=message.contents, + role=message.role, + response_id=task.id, + message_id=artifact_id, + raw_representation=task, + ) + else: + # Empty task + yield AgentRunResponseUpdate( + contents=[], + role=Role.ASSISTANT, + response_id=task.id, + raw_representation=task, + ) + else: + # Unknown response type + msg = f"Only Message and Task responses are supported from A2A agents. Received: {type(item)}" + raise NotImplementedError(msg) + + def _chat_message_to_a2a_message(self, message: ChatMessage) -> A2AMessage: + """Convert a ChatMessage to an A2A Message. + + Transforms Agent Framework ChatMessage objects into A2A protocol Messages by: + - Converting all message contents to appropriate A2A Part types + - Mapping text content to TextPart objects + - Converting file references (URI/data/hosted_file) to FilePart objects + - Preserving metadata and additional properties from the original message + - Setting the role to 'user' as framework messages are treated as user input + """ + parts: list[A2APart] = [] + if not message.contents: + raise ValueError("ChatMessage.contents is empty; cannot convert to A2AMessage.") + + # Process ALL contents + for content in message.contents: + match content.type: + case "text": + parts.append( + A2APart( + root=TextPart( + text=content.text, + metadata=content.additional_properties, + ) + ) + ) + case "error": + parts.append( + A2APart( + root=TextPart( + text=content.message or "An error occurred.", + metadata=content.additional_properties, + ) + ) + ) + case "uri": + parts.append( + A2APart( + root=FilePart( + file=FileWithUri( + uri=content.uri, + mime_type=content.media_type, + ), + metadata=content.additional_properties, + ) + ) + ) + case "data": + parts.append( + A2APart( + root=FilePart( + file=FileWithBytes( + bytes=_get_uri_data(content.uri), + mime_type=content.media_type, + ), + metadata=content.additional_properties, + ) + ) + ) + case "hosted_file": + parts.append( + A2APart( + root=FilePart( + file=FileWithUri( + uri=content.file_id, + mime_type=None, # HostedFileContent doesn't specify media_type + ), + metadata=content.additional_properties, + ) + ) + ) + case _: + raise ValueError(f"Unknown content type: {content.type}") + + return A2AMessage( + role=A2ARole("user"), + parts=parts, + message_id=message.message_id or uuid.uuid4().hex, + metadata=message.additional_properties, + ) + + def _a2a_parts_to_contents(self, parts: Sequence[A2APart]) -> list[Contents]: + """Convert A2A Parts to Agent Framework Contents. + + Transforms A2A protocol Parts into framework-native Content objects, + handling text, file (URI/bytes), and data parts with metadata preservation. + """ + contents: list[Contents] = [] + for part in parts: + inner_part = part.root + match inner_part.kind: + case "text": + contents.append( + TextContent( + text=inner_part.text, + additional_properties=inner_part.metadata, + raw_representation=inner_part, + ) + ) + case "file": + if isinstance(inner_part.file, FileWithUri): + contents.append( + UriContent( + uri=inner_part.file.uri, + media_type=inner_part.file.mime_type or "", + additional_properties=inner_part.metadata, + raw_representation=inner_part, + ) + ) + elif isinstance(inner_part.file, FileWithBytes): + contents.append( + DataContent( + data=base64.b64decode(inner_part.file.bytes), + media_type=inner_part.file.mime_type or "", + additional_properties=inner_part.metadata, + raw_representation=inner_part, + ) + ) + case "data": + contents.append( + TextContent( + text=json.dumps(inner_part.data), + additional_properties=inner_part.metadata, + raw_representation=inner_part, + ) + ) + case _: + raise ValueError(f"Unknown Part kind: {inner_part.kind}") + return contents + + def _task_to_chat_messages(self, task: Task) -> list[ChatMessage]: + """Convert A2A Task artifacts to ChatMessages with ASSISTANT role.""" + messages: list[ChatMessage] = [] + + if task.artifacts is not None: + for artifact in task.artifacts: + messages.append(self._artifact_to_chat_message(artifact)) + + return messages + + def _artifact_to_chat_message(self, artifact: Artifact) -> ChatMessage: + """Convert A2A Artifact to ChatMessage using part contents.""" + contents = self._a2a_parts_to_contents(artifact.parts) + return ChatMessage( + role=Role.ASSISTANT, + contents=contents, + raw_representation=artifact, + ) diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml new file mode 100644 index 0000000000..56e481d0be --- /dev/null +++ b/python/packages/a2a/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "agent-framework-a2a" +description = "A2A integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "0.1.0b1" +license-files = ["LICENSE"] +urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Framework :: Pydantic :: 2", + "Typing :: Typed", +] +dependencies = [ + "agent-framework", + "a2a-sdk>=0.3.5", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*" +] +timeout = 120 + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extend = "../../pyproject.toml" +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +[tool.bandit] +targets = ["agent_framework_a2a"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" +[tool.poe.tasks] +mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a" +test = "pytest --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests" + +[build-system] +requires = ["flit-core >= 3.9,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py new file mode 100644 index 0000000000..bdb07a214b --- /dev/null +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -0,0 +1,498 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import AsyncIterator +from typing import Any +from unittest.mock import AsyncMock, MagicMock +from uuid import uuid4 + +from a2a.types import Artifact, DataPart, FilePart, FileWithUri, Message, Part, Task, TaskState, TaskStatus, TextPart +from a2a.types import Role as A2ARole +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + ChatMessage, + DataContent, + ErrorContent, + HostedFileContent, + Role, + TextContent, + UriContent, +) +from agent_framework.a2a import A2AAgent +from pytest import fixture, raises + +from agent_framework_a2a._agent import _get_uri_data # type: ignore + + +class MockA2AClient: + """Mock implementation of A2A Client for testing.""" + + def __init__(self) -> None: + self.call_count: int = 0 + self.responses: list[Any] = [] + + def add_message_response(self, message_id: str, text: str, role: str = "agent") -> None: + """Add a mock Message response.""" + + # Create actual TextPart instance and wrap it in Part + text_part = Part(root=TextPart(text=text)) + + # Create actual Message instance + message = Message( + message_id=message_id, role=A2ARole.agent if role == "agent" else A2ARole.user, parts=[text_part] + ) + self.responses.append(message) + + def add_task_response(self, task_id: str, artifacts: list[dict[str, Any]]) -> None: + """Add a mock Task response.""" + # Create mock artifacts + mock_artifacts = [] + for artifact_data in artifacts: + # Create actual TextPart instance and wrap it in Part + text_part = Part(root=TextPart(text=artifact_data.get("content", "Test content"))) + + artifact = Artifact( + artifact_id=artifact_data.get("id", str(uuid4())), + name=artifact_data.get("name", "test-artifact"), + description=artifact_data.get("description", "Test artifact"), + parts=[text_part], + ) + mock_artifacts.append(artifact) + + # Create task status + status = TaskStatus(state=TaskState.completed, message=None) + + # Create actual Task instance + task = Task( + id=task_id, context_id="test-context", status=status, artifacts=mock_artifacts if mock_artifacts else None + ) + + # Mock the ClientEvent tuple format + update_event = None # No specific update event for completed tasks + client_event = (task, update_event) + self.responses.append(client_event) + + async def send_message(self, message: Any) -> AsyncIterator[Any]: + """Mock send_message method that yields responses.""" + self.call_count += 1 + + if self.responses: + response = self.responses.pop(0) + yield response + + +@fixture +def mock_a2a_client() -> MockA2AClient: + """Fixture that provides a mock A2A client.""" + return MockA2AClient() + + +@fixture +def a2a_agent(mock_a2a_client: MockA2AClient) -> A2AAgent: + """Fixture that provides an A2AAgent with a mock client.""" + return A2AAgent.model_construct(name="Test Agent", id="test-agent", client=mock_a2a_client, _http_client=None) + + +def test_a2a_agent_initialization_with_client(mock_a2a_client: MockA2AClient) -> None: + """Test A2AAgent initialization with provided client.""" + # Use model_construct to bypass Pydantic validation for mock objects + agent = A2AAgent.model_construct( + name="Test Agent", id="test-agent-123", description="A test agent", client=mock_a2a_client, _http_client=None + ) + + assert agent.name == "Test Agent" + assert agent.id == "test-agent-123" + assert agent.description == "A test agent" + assert agent.client == mock_a2a_client + + +def test_a2a_agent_initialization_without_client_raises_error() -> None: + """Test A2AAgent initialization without client or URL raises ValueError.""" + with raises(ValueError, match="Either agent_card or url must be provided"): + A2AAgent(name="Test Agent") + + +async def test_run_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test run() method with immediate Message response.""" + mock_a2a_client.add_message_response("msg-123", "Hello from agent!", "agent") + + response = await a2a_agent.run("Hello agent") + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 1 + assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].text == "Hello from agent!" + assert response.response_id == "msg-123" + assert mock_a2a_client.call_count == 1 + + +async def test_run_with_task_response_single_artifact(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test run() method with Task response containing single artifact.""" + artifacts = [{"id": "art-1", "content": "Generated report content"}] + mock_a2a_client.add_task_response("task-456", artifacts) + + response = await a2a_agent.run("Generate a report") + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 1 + assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].text == "Generated report content" + assert response.response_id == "task-456" + assert mock_a2a_client.call_count == 1 + + +async def test_run_with_task_response_multiple_artifacts(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test run() method with Task response containing multiple artifacts.""" + artifacts = [ + {"id": "art-1", "content": "First artifact content"}, + {"id": "art-2", "content": "Second artifact content"}, + {"id": "art-3", "content": "Third artifact content"}, + ] + mock_a2a_client.add_task_response("task-789", artifacts) + + response = await a2a_agent.run("Generate multiple outputs") + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 3 + + assert response.messages[0].text == "First artifact content" + assert response.messages[1].text == "Second artifact content" + assert response.messages[2].text == "Third artifact content" + + # All should be assistant messages + for message in response.messages: + assert message.role == Role.ASSISTANT + + assert response.response_id == "task-789" + + +async def test_run_with_task_response_no_artifacts(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test run() method with Task response containing no artifacts.""" + mock_a2a_client.add_task_response("task-empty", []) + + response = await a2a_agent.run("Do something with no output") + + assert isinstance(response, AgentRunResponse) + assert response.response_id == "task-empty" + + +async def test_run_with_unknown_response_type_raises_error(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test run() method with unknown response type raises NotImplementedError.""" + mock_a2a_client.responses.append("invalid_response") + + with raises(NotImplementedError, match="Only Message and Task responses are supported"): + await a2a_agent.run("Test message") + + +def test_task_to_chat_messages_empty_artifacts(a2a_agent: A2AAgent) -> None: + """Test _task_to_chat_messages with task containing no artifacts.""" + task = MagicMock() + task.artifacts = None + + result = a2a_agent._task_to_chat_messages(task) + + assert len(result) == 0 + + +def test_task_to_chat_messages_with_artifacts(a2a_agent: A2AAgent) -> None: + """Test _task_to_chat_messages with task containing artifacts.""" + task = MagicMock() + + # Create mock artifacts + artifact1 = MagicMock() + artifact1.artifact_id = "art-1" + text_part1 = MagicMock() + text_part1.root = MagicMock() + text_part1.root.kind = "text" + text_part1.root.text = "Content 1" + text_part1.root.metadata = None + artifact1.parts = [text_part1] + + artifact2 = MagicMock() + artifact2.artifact_id = "art-2" + text_part2 = MagicMock() + text_part2.root = MagicMock() + text_part2.root.kind = "text" + text_part2.root.text = "Content 2" + text_part2.root.metadata = None + artifact2.parts = [text_part2] + + task.artifacts = [artifact1, artifact2] + + result = a2a_agent._task_to_chat_messages(task) + + assert len(result) == 2 + assert result[0].text == "Content 1" + assert result[1].text == "Content 2" + assert all(msg.role == Role.ASSISTANT for msg in result) + + +def test_artifact_to_chat_message(a2a_agent: A2AAgent) -> None: + """Test _artifact_to_chat_message conversion.""" + artifact = MagicMock() + artifact.artifact_id = "test-artifact" + + text_part = MagicMock() + text_part.root = MagicMock() + text_part.root.kind = "text" + text_part.root.text = "Artifact content" + text_part.root.metadata = None + + artifact.parts = [text_part] + + result = a2a_agent._artifact_to_chat_message(artifact) + + assert isinstance(result, ChatMessage) + assert result.role == Role.ASSISTANT + assert result.text == "Artifact content" + assert result.raw_representation == artifact + + +def test_get_uri_data_valid_uri() -> None: + """Test _get_uri_data with valid data URI.""" + + uri = "data:application/json;base64,eyJ0ZXN0IjoidmFsdWUifQ==" + result = _get_uri_data(uri) + assert result == "eyJ0ZXN0IjoidmFsdWUifQ==" + + +def test_get_uri_data_invalid_uri() -> None: + """Test _get_uri_data with invalid URI format.""" + + with raises(ValueError, match="Invalid data URI format"): + _get_uri_data("not-a-valid-data-uri") + + +def test_a2a_parts_to_contents_conversion(a2a_agent: A2AAgent) -> None: + """Test A2A parts to contents conversion.""" + + agent = A2AAgent.model_construct(name="Test Agent", client=MockA2AClient(), _http_client=None) + + # Create A2A parts + parts = [Part(root=TextPart(text="First part")), Part(root=TextPart(text="Second part"))] + + # Convert to contents + contents = agent._a2a_parts_to_contents(parts) + + # Verify conversion + assert len(contents) == 2 + assert isinstance(contents[0], TextContent) + assert isinstance(contents[1], TextContent) + assert contents[0].text == "First part" + assert contents[1].text == "Second part" + + +def test_chat_message_to_a2a_message_with_error_content(a2a_agent: A2AAgent) -> None: + """Test _chat_message_to_a2a_message with ErrorContent.""" + + # Create ChatMessage with ErrorContent + error_content = ErrorContent(message="Test error message") + message = ChatMessage(role=Role.USER, contents=[error_content]) + + # Convert to A2A message + a2a_message = a2a_agent._chat_message_to_a2a_message(message) + + # Verify conversion + assert len(a2a_message.parts) == 1 + assert a2a_message.parts[0].root.text == "Test error message" + + +def test_chat_message_to_a2a_message_with_uri_content(a2a_agent: A2AAgent) -> None: + """Test _chat_message_to_a2a_message with UriContent.""" + + # Create ChatMessage with UriContent + uri_content = UriContent(uri="http://example.com/file.pdf", media_type="application/pdf") + message = ChatMessage(role=Role.USER, contents=[uri_content]) + + # Convert to A2A message + a2a_message = a2a_agent._chat_message_to_a2a_message(message) + + # Verify conversion + assert len(a2a_message.parts) == 1 + assert a2a_message.parts[0].root.file.uri == "http://example.com/file.pdf" + assert a2a_message.parts[0].root.file.mime_type == "application/pdf" + + +def test_chat_message_to_a2a_message_with_data_content(a2a_agent: A2AAgent) -> None: + """Test _chat_message_to_a2a_message with DataContent.""" + + # Create ChatMessage with DataContent (base64 data URI) + data_content = DataContent(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain") + message = ChatMessage(role=Role.USER, contents=[data_content]) + + # Convert to A2A message + a2a_message = a2a_agent._chat_message_to_a2a_message(message) + + # Verify conversion + assert len(a2a_message.parts) == 1 + assert a2a_message.parts[0].root.file.bytes == "SGVsbG8gV29ybGQ=" + assert a2a_message.parts[0].root.file.mime_type == "text/plain" + + +def test_chat_message_to_a2a_message_empty_contents_raises_error(a2a_agent: A2AAgent) -> None: + """Test _chat_message_to_a2a_message with empty contents raises ValueError.""" + # Create ChatMessage with no contents + message = ChatMessage(role=Role.USER, contents=[]) + + # Should raise ValueError for empty contents + with raises(ValueError, match="ChatMessage.contents is empty"): + a2a_agent._chat_message_to_a2a_message(message) + + +async def test_run_stream_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test run_stream() method with immediate Message response.""" + mock_a2a_client.add_message_response("msg-stream-123", "Streaming response from agent!", "agent") + + # Collect streaming updates + updates: list[AgentRunResponseUpdate] = [] + async for update in a2a_agent.run_stream("Hello agent"): + updates.append(update) + + # Verify streaming response + assert len(updates) == 1 + assert isinstance(updates[0], AgentRunResponseUpdate) + assert updates[0].role == Role.ASSISTANT + assert len(updates[0].contents) == 1 + + content = updates[0].contents[0] + assert isinstance(content, TextContent) + assert content.text == "Streaming response from agent!" + + assert updates[0].response_id == "msg-stream-123" + assert mock_a2a_client.call_count == 1 + + +async def test_context_manager_cleanup() -> None: + """Test context manager cleanup of http client.""" + + # Create mock http client that tracks aclose calls + mock_http_client = AsyncMock() + mock_a2a_client = MagicMock() + + agent = A2AAgent.model_construct(client=mock_a2a_client, _http_client=mock_http_client) + + # Test context manager cleanup + async with agent: + pass + + # Verify aclose was called + mock_http_client.aclose.assert_called_once() + + +async def test_context_manager_no_cleanup_when_no_http_client() -> None: + """Test context manager when _http_client is None.""" + + mock_a2a_client = MagicMock() + + agent = A2AAgent.model_construct(client=mock_a2a_client, _http_client=None) + + # This should not raise any errors + async with agent: + pass + + +def test_chat_message_to_a2a_message_with_multiple_contents() -> None: + """Test conversion of ChatMessage with multiple contents.""" + + agent = A2AAgent.model_construct(client=MagicMock(), _http_client=None) + + # Create message with multiple content types + message = ChatMessage( + role=Role.USER, + contents=[ + TextContent(text="Here's the analysis:"), + DataContent(data=b"binary data", media_type="application/octet-stream"), + UriContent(uri="https://example.com/image.png", media_type="image/png"), + TextContent(text='{"structured": "data"}'), + ], + ) + + result = agent._chat_message_to_a2a_message(message) + + # Should have converted all 4 contents to parts + assert len(result.parts) == 4 + + # Check each part type + assert result.parts[0].root.kind == "text" # Regular text + assert result.parts[1].root.kind == "file" # Binary data + assert result.parts[2].root.kind == "file" # URI content + assert result.parts[3].root.kind == "text" # JSON text remains as text (no parsing) + + +def test_a2a_parts_to_contents_with_data_part() -> None: + """Test conversion of A2A DataPart.""" + + agent = A2AAgent.model_construct(client=MagicMock(), _http_client=None) + + # Create DataPart + data_part = Part(root=DataPart(data={"key": "value", "number": 42}, metadata={"source": "test"})) + + contents = agent._a2a_parts_to_contents([data_part]) + + assert len(contents) == 1 + + assert isinstance(contents[0], TextContent) + assert contents[0].text == '{"key": "value", "number": 42}' + assert contents[0].additional_properties == {"source": "test"} + + +def test_a2a_parts_to_contents_unknown_part_kind() -> None: + """Test error handling for unknown A2A part kind.""" + agent = A2AAgent.model_construct(client=MagicMock(), _http_client=None) + + # Create a mock part with unknown kind + mock_part = MagicMock() + mock_part.root.kind = "unknown_kind" + + with raises(ValueError, match="Unknown Part kind: unknown_kind"): + agent._a2a_parts_to_contents([mock_part]) + + +def test_chat_message_to_a2a_message_with_hosted_file() -> None: + """Test conversion of ChatMessage with HostedFileContent to A2A message.""" + + agent = A2AAgent.model_construct(client=MagicMock(), _http_client=None) + + # Create message with hosted file content + message = ChatMessage( + role=Role.USER, + contents=[HostedFileContent(file_id="hosted://storage/document.pdf")], + ) + + result = agent._chat_message_to_a2a_message(message) # noqa: SLF001 + + # Verify the conversion + assert len(result.parts) == 1 + part = result.parts[0] + assert part.root.kind == "file" + + # Verify it's a FilePart with FileWithUri + + assert isinstance(part.root, FilePart) + assert isinstance(part.root.file, FileWithUri) + assert part.root.file.uri == "hosted://storage/document.pdf" + assert part.root.file.mime_type is None # HostedFileContent doesn't specify media_type + + +def test_a2a_parts_to_contents_with_hosted_file_uri() -> None: + """Test conversion of A2A FilePart with hosted file URI back to UriContent.""" + + agent = A2AAgent.model_construct(client=MagicMock(), _http_client=None) + + # Create FilePart with hosted file URI (simulating what A2A would send back) + file_part = Part( + root=FilePart( + file=FileWithUri( + uri="hosted://storage/document.pdf", + mime_type=None, + ) + ) + ) + + contents = agent._a2a_parts_to_contents([file_part]) # noqa: SLF001 + + assert len(contents) == 1 + + assert isinstance(contents[0], UriContent) + assert contents[0].uri == "hosted://storage/document.pdf" + assert contents[0].media_type == "" # Converted None to empty string diff --git a/python/packages/main/agent_framework/a2a/__init__.py b/python/packages/main/agent_framework/a2a/__init__.py new file mode 100644 index 0000000000..f66cb0400c --- /dev/null +++ b/python/packages/main/agent_framework/a2a/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib +from typing import Any + +PACKAGE_NAME = "agent_framework_a2a" +PACKAGE_EXTRA = "a2a" +_IMPORTS = ["__version__", "A2AAgent"] + + +def __getattr__(name: str) -> Any: + if name in _IMPORTS: + try: + return getattr(importlib.import_module(PACKAGE_NAME), name) + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + f"The '{PACKAGE_EXTRA}' extra is not installed, " + f"please do `pip install agent-framework[{PACKAGE_EXTRA}]`" + ) from exc + raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.") + + +def __dir__() -> list[str]: + return _IMPORTS diff --git a/python/packages/main/agent_framework/a2a/__init__.pyi b/python/packages/main/agent_framework/a2a/__init__.pyi new file mode 100644 index 0000000000..09e3c94b57 --- /dev/null +++ b/python/packages/main/agent_framework/a2a/__init__.pyi @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft. All rights reserved. + +from agent_framework_a2a import A2AAgent, __version__ + +__all__ = ["A2AAgent", "__version__"] diff --git a/python/packages/main/pyproject.toml b/python/packages/main/pyproject.toml index 5977dccbe5..713e157488 100644 --- a/python/packages/main/pyproject.toml +++ b/python/packages/main/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "opentelemetry-exporter-otlp-proto-grpc>=1.36.0", "opentelemetry-semantic-conventions-ai>=0.4.13", "aiofiles>=24.1.0", - "azure-identity>=1,<2" + "azure-identity>=1,<2", ] [project.optional-dependencies] @@ -63,6 +63,9 @@ runtime = [ mem0 = [ "agent-framework-mem0" ] +a2a = [ + "agent-framework-a2a" +] devui = [ "agent-framework-devui" ] @@ -71,6 +74,7 @@ all = [ "agent-framework-azure-ai", "agent-framework-runtime", "agent-framework-mem0", + "agent-framework-a2a", "agent-framework-redis", "agent-framework-devui", "graphviz>=0.20.0" @@ -91,6 +95,7 @@ fallback-version = "0.0.0" testpaths = [ 'tests', 'packages/main/tests', + 'packages/a2a/tests', 'packages/azure-ai/tests', 'packages/copilotstudio/tests', 'packages/mem0/tests', diff --git a/python/pyproject.toml b/python/pyproject.toml index 6211f8b34f..2e2251c595 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -5,6 +5,7 @@ version = "0.0.0" requires-python = ">=3.10" dependencies = [ "agent-framework", + "agent-framework-a2a", "agent-framework-azure-ai", "agent-framework-copilotstudio", "agent-framework-mem0", @@ -54,6 +55,7 @@ exclude = [ "packages/agent_framework_project.egg-info" ] [tool.uv.sources] agent-framework = { workspace = true } +agent-framework-a2a = { workspace = true } agent-framework-azure-ai = { workspace = true } agent-framework-copilotstudio = { workspace = true } agent-framework-lab = { workspace = true } @@ -194,6 +196,7 @@ pre-commit-check = ["fmt", "lint", "pyright", "markdown-code-lint", "samples-cod cmd = """ pytest --import-mode=importlib --cov=agent_framework +--cov=agent_framework_a2a --cov=agent_framework_azure_ai --cov=agent_framework_copilotstudio --cov=agent_framework_mem0 diff --git a/python/samples/getting_started/agents/a2a/README.md b/python/samples/getting_started/agents/a2a/README.md new file mode 100644 index 0000000000..8098991db1 --- /dev/null +++ b/python/samples/getting_started/agents/a2a/README.md @@ -0,0 +1,31 @@ +# A2A Agent Examples + +This folder contains examples demonstrating how to create and use agents with the A2A (Agent-to-Agent) protocol from the `agent_framework` package to communicate with remote A2A agents. + +For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/ +## Examples + +| File | Description | +|------|-------------| +| [`agent_with_a2a.py`](agent_with_a2a.py) | The simplest way to connect to and use a single A2A agent. Demonstrates agent discovery via agent cards and basic message exchange using the A2A protocol. | + +## Environment Variables + +Make sure to set the following environment variables before running the example: + +### Required +- `A2A_AGENT_HOST`: URL of a single A2A agent (for simple sample, e.g., `http://localhost:5001/`) + + +## Quick Testing with .NET A2A Servers + +For quick testing and demonstration, you can use the pre-built .NET A2A servers from this repository: + +**Quick Testing Reference**: Use the .NET A2A Client Server sample at: +`..\agent-framework\dotnet\samples\A2AClientServer` + +### Run Python A2A Sample +```powershell +# Simple A2A sample (single agent) +uv run python agent_with_a2a.py +``` \ No newline at end of file diff --git a/python/samples/getting_started/agents/a2a/agent_with_a2a.py b/python/samples/getting_started/agents/a2a/agent_with_a2a.py new file mode 100644 index 0000000000..5c9ace4eb4 --- /dev/null +++ b/python/samples/getting_started/agents/a2a/agent_with_a2a.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +import httpx +from a2a.client import A2ACardResolver +from agent_framework.a2a import A2AAgent + +""" +Agent-to-Agent (A2A) Protocol Integration Sample + +This sample demonstrates how to connect to and communicate with external agents using +the A2A protocol. A2A is a standardized communication protocol that enables interoperability +between different agent systems, allowing agents built with different frameworks and +technologies to communicate seamlessly. + +For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/ + +Key concepts demonstrated: +- Discovering A2A-compliant agents using AgentCard resolution +- Creating A2AAgent instances to wrap external A2A endpoints +- Converting Agent Framework messages to A2A protocol format +- Handling A2A responses (Messages and Tasks) back to framework types + +To run this sample: +1. Set the A2A_AGENT_HOST environment variable to point to an A2A-compliant agent endpoint + Example: export A2A_AGENT_HOST="https://your-a2a-agent.example.com" +2. Ensure the target agent exposes its AgentCard at /.well-known/agent.json +3. Run: uv run python agent_with_a2a.py + +The sample will: +- Connect to the specified A2A agent endpoint +- Retrieve and parse the agent's capabilities via its AgentCard +- Send a message using the A2A protocol +- Display the agent's response + +Visit the README.md for more details on setting up and running A2A agents. +""" + + +async def main(): + """Demonstrates connecting to and communicating with an A2A-compliant agent.""" + # Get A2A agent host from environment + a2a_agent_host = os.getenv("A2A_AGENT_HOST") + if not a2a_agent_host: + raise ValueError("A2A_AGENT_HOST environment variable is not set") + + print(f"Connecting to A2A agent at: {a2a_agent_host}") + + # Initialize A2ACardResolver + async with httpx.AsyncClient(timeout=60.0) as http_client: + resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host) + + # Get agent card + agent_card = await resolver.get_agent_card(relative_card_path="/.well-known/agent.json") + print(f"Found agent: {agent_card.name} - {agent_card.description}") + + # Create A2A agent instance + agent = A2AAgent( + name=agent_card.name, description=agent_card.description, agent_card=agent_card, url=a2a_agent_host + ) + + # Invoke the agent and output the result + print("\nSending message to A2A agent...") + response = await agent.run("Tell me a joke about a pirate.") + + # Print the response + print("\nAgent Response:") + for message in response.messages: + print(message.text) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/uv.lock b/python/uv.lock index 18fab2d9cc..45a525266c 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -24,6 +24,7 @@ supported-markers = [ [manifest] members = [ "agent-framework", + "agent-framework-a2a", "agent-framework-azure-ai", "agent-framework-copilotstudio", "agent-framework-devui", @@ -34,6 +35,22 @@ members = [ "agent-framework-runtime", ] +[[package]] +name = "a2a-sdk" +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx-sse", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/b6ecb58f44459a24f1c260e91304e1ddbb7a8e213f1f82cc4c074f66e9bb/a2a_sdk-0.3.7.tar.gz", hash = "sha256:795aa2bd2cfb3c9e8654a1352bf5f75d6cf1205b262b1bf8f4003b5308267ea2", size = 223426, upload-time = "2025-09-23T16:27:29.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/27/9cf8c6de4ae71e9c98ec96b3304449d5d0cd36ec3b95e66b6e7f58a9e571/a2a_sdk-0.3.7-py3-none-any.whl", hash = "sha256:0813b8fd7add427b2b56895cf28cae705303cf6d671b305c0aac69987816e03e", size = 137957, upload-time = "2025-09-23T16:27:27.546Z" }, +] + [[package]] name = "addict" version = "2.4.0" @@ -64,7 +81,11 @@ dependencies = [ ] [package.optional-dependencies] +a2a = [ + { name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] all = [ + { name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-azure-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-devui", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -103,6 +124,8 @@ viz = [ [package.metadata] requires-dist = [ + { name = "agent-framework-a2a", marker = "extra == 'a2a'", editable = "packages/a2a" }, + { name = "agent-framework-a2a", marker = "extra == 'all'", editable = "packages/a2a" }, { name = "agent-framework-azure-ai", marker = "extra == 'all'", editable = "packages/azure-ai" }, { name = "agent-framework-azure-ai", marker = "extra == 'azure'", editable = "packages/azure-ai" }, { name = "agent-framework-azure-ai", marker = "extra == 'azure-ai'", editable = "packages/azure-ai" }, @@ -133,7 +156,22 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2,<3" }, { name = "typing-extensions" }, ] -provides-extras = ["azure-ai", "azure", "microsoft-copilotstudio", "microsoft", "redis", "viz", "runtime", "mem0", "devui", "all"] +provides-extras = ["azure-ai", "azure", "microsoft-copilotstudio", "microsoft", "redis", "viz", "runtime", "mem0", "a2a", "devui", "all"] + +[[package]] +name = "agent-framework-a2a" +version = "0.1.0b1" +source = { editable = "packages/a2a" } +dependencies = [ + { name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", specifier = ">=0.3.5" }, + { name = "agent-framework", editable = "packages/main" }, +] [[package]] name = "agent-framework-azure-ai" @@ -269,6 +307,7 @@ version = "0.0.0" source = { virtual = "." } dependencies = [ { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-azure-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-devui", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -304,6 +343,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "agent-framework", editable = "packages/main" }, + { name = "agent-framework-a2a", editable = "packages/a2a" }, { name = "agent-framework-azure-ai", editable = "packages/azure-ai" }, { name = "agent-framework-copilotstudio", editable = "packages/copilotstudio" }, { name = "agent-framework-devui", editable = "packages/devui" }, @@ -712,6 +752,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -1596,6 +1645,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, ] +[[package]] +name = "google-api-core" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "proto-plus", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "rsa", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.70.0" @@ -3707,6 +3786,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, +] + [[package]] name = "protobuf" version = "5.29.5" @@ -3750,6 +3841,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/bc/056b4751229c92b58296f8eccd262a448f799b3044510f2357742eeeae98/py2docfx-0.1.21-py3-none-any.whl", hash = "sha256:ce782d503593c79fc49324d3b102fe38b1cf5de9e13dfe582abe6d1387a67999", size = 11410660, upload-time = "2025-09-23T03:44:18.458Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -4503,6 +4615,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, ] +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + [[package]] name = "ruff" version = "0.13.2"