-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: A2A (Agent-to-Agent) protocol support #3567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
+2,972
−3
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| """ | ||
| Example: Calling an A2A agent as a tool from an OpenAI agent. | ||
|
|
||
| This example demonstrates using ``A2AClientTool`` to call a remote A2A agent | ||
| as a function tool. It assumes you have an A2A agent running at the given URL. | ||
|
|
||
| To try it out: | ||
|
|
||
| 1. Start a sample A2A server (e.g. the hello-world agent from a2a-sdk): | ||
| ``cd a2a-python/samples && python cli.py server`` | ||
|
|
||
| 2. Run this script: | ||
| ``python examples/a2a/basic_client_example.py`` | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
|
|
||
| from agents import Agent, Runner | ||
| from agents.extensions.a2a import A2AClientTool | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| # Create a tool that wraps a remote A2A agent. | ||
| # from_url() fetches the AgentCard at .well-known/agent-card.json | ||
| research_tool = await A2AClientTool.from_url( | ||
| url="http://localhost:10000", | ||
| tool_name="research_agent", | ||
| tool_description=( | ||
| "Ask the research agent to find and summarize information. " | ||
| "Use when you need external knowledge." | ||
| ), | ||
| ) | ||
|
|
||
| orchestrator = Agent( | ||
| name="Orchestrator", | ||
| instructions="You are an orchestrator. Use the research_agent tool for external queries.", | ||
| tools=[research_tool.as_function_tool()], | ||
| ) | ||
|
|
||
| result = await Runner.run( | ||
| orchestrator, | ||
| "What are the latest developments in quantum computing?", | ||
| ) | ||
| print(f"Final output: {result.final_output}") | ||
|
|
||
| # Clean up | ||
| await research_tool.close() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| """ | ||
| A2A (Agent-to-Agent) protocol integration for the OpenAI Agents SDK. | ||
|
|
||
| This extension enables bidirectional interoperability between OpenAI Agents | ||
| and any A2A-compatible agent (built with any framework, in any language): | ||
|
|
||
| - **A2A Client Tool**: Call external A2A agents as tools from your OpenAI agent. | ||
| - **A2A Server Agent**: Expose your OpenAI agent as an A2A service so other | ||
| agents can call it. | ||
|
|
||
| The A2A protocol is defined by Google at https://github.com/google/A2A. | ||
|
|
||
| Dependencies | ||
| ------------ | ||
| This module requires the ``a2a-sdk`` package. Install it with:: | ||
|
|
||
| pip install openai-agents[a2a] | ||
|
|
||
| or directly:: | ||
|
|
||
| pip install a2a-sdk | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from importlib import import_module | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from agents.extensions.memory._optional_imports import raise_optional_dependency_error | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ._agent_card import generate_agent_card | ||
| from ._client_tool import A2AClientTool | ||
| from ._server_executor import A2AServerAgent | ||
| from ._converter import ( | ||
| a2a_context_to_openai_input, | ||
| a2a_history_to_openai_input_items, | ||
| a2a_message_to_openai_input_items, | ||
| openai_error_to_failed_task, | ||
| openai_final_output_to_artifacts, | ||
| openai_items_to_a2a_messages, | ||
| openai_run_result_to_task, | ||
| openai_stream_event_to_task_status, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "A2AClientTool", | ||
| "A2AServerAgent", | ||
| "generate_agent_card", | ||
| "a2a_context_to_openai_input", | ||
| "a2a_history_to_openai_input_items", | ||
| "a2a_message_to_openai_input_items", | ||
| "openai_error_to_failed_task", | ||
| "openai_final_output_to_artifacts", | ||
| "openai_items_to_a2a_messages", | ||
| "openai_run_result_to_task", | ||
| "openai_stream_event_to_task_status", | ||
| ] | ||
|
|
||
| _LAZY_EXPORTS: dict[str, tuple[str, tuple[str, str] | None]] = { | ||
| "A2AClientTool": ("._client_tool", ("a2a-sdk", "a2a")), | ||
| "A2AServerAgent": ("._server_executor", ("a2a-sdk", "a2a")), | ||
| "generate_agent_card": ("._agent_card", ("a2a-sdk", "a2a")), | ||
| "a2a_context_to_openai_input": ("._converter", ("a2a-sdk", "a2a")), | ||
| "a2a_history_to_openai_input_items": ("._converter", ("a2a-sdk", "a2a")), | ||
| "a2a_message_to_openai_input_items": ("._converter", ("a2a-sdk", "a2a")), | ||
| "openai_error_to_failed_task": ("._converter", ("a2a-sdk", "a2a")), | ||
| "openai_final_output_to_artifacts": ("._converter", ("a2a-sdk", "a2a")), | ||
| "openai_items_to_a2a_messages": ("._converter", ("a2a-sdk", "a2a")), | ||
| "openai_run_result_to_task": ("._converter", ("a2a-sdk", "a2a")), | ||
| "openai_stream_event_to_task_status": ("._converter", ("a2a-sdk", "a2a")), | ||
| } | ||
|
|
||
|
|
||
| def __getattr__(name: str) -> Any: | ||
| if name not in _LAZY_EXPORTS: | ||
| raise AttributeError(f"module {__name__!r} has no attribute {name!r}") | ||
|
|
||
| module_name, optional_dependency = _LAZY_EXPORTS[name] | ||
| try: | ||
| module = import_module(module_name, __name__) | ||
| except ModuleNotFoundError as e: | ||
| if optional_dependency is None: | ||
| raise ImportError(f"Failed to import {name}: {e}") from e | ||
| dependency_name, extra_name = optional_dependency | ||
| raise_optional_dependency_error( | ||
| name, | ||
| dependency_name=dependency_name, | ||
| extra_name=extra_name, | ||
| cause=e, | ||
| ) | ||
|
|
||
| value = getattr(module, name) | ||
| # Cache for subsequent access. | ||
| globals()[name] = value | ||
| return value |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| """ | ||
| AgentCard generator — build an A2A ``AgentCard`` from OpenAI Agent metadata. | ||
| This module inspects an OpenAI ``Agent`` instance and produces a compliant | ||
| A2A ``AgentCard`` that describes its name, description, skills (derived from | ||
| tools and handoffs), capabilities, and supported interfaces. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from a2a.types.a2a_pb2 import ( # type: ignore[import-untyped] | ||
| AgentCapabilities, | ||
| AgentCard, | ||
| AgentInterface, | ||
| AgentProvider, | ||
| AgentSkill, | ||
| ) | ||
| from agents.agent import Agent | ||
|
|
||
|
|
||
| def generate_agent_card( | ||
| agent: Agent[Any], | ||
| *, | ||
| url: str, | ||
| provider: AgentProvider | None = None, | ||
| capabilities: AgentCapabilities | None = None, | ||
| supported_interfaces: list[AgentInterface] | None = None, | ||
| version: str = "1.0.0", | ||
| ) -> AgentCard: | ||
| """Generate an A2A ``AgentCard`` from an OpenAI ``Agent`` instance. | ||
| The card includes: | ||
| - The agent's ``name`` and ``instructions`` (or ``handoff_description``) | ||
| as the card's ``description``. | ||
| - One ``AgentSkill`` per ``FunctionTool`` (and per ``Agent.as_tool()`` | ||
| handoff) with its name, description, and tags. | ||
| - Default ``input_modes`` / ``output_modes`` set to ``["text"]``. | ||
| Args: | ||
| agent: The OpenAI ``Agent`` to describe. | ||
| url: The base URL where the agent will be served. | ||
| provider: Optional ``AgentProvider`` (organisation metadata). | ||
| capabilities: Optional ``AgentCapabilities``; defaults to streaming | ||
| enabled with no push notifications. | ||
| supported_interfaces: Optional list of ``AgentInterface`` entries; | ||
| defaults to a single JSON-RPC interface at ``url``. | ||
| version: Version string for the agent card (default ``"1.0.0"``). | ||
| Returns: | ||
| A populated A2A ``AgentCard`` protobuf message. | ||
| """ | ||
| from a2a.types.a2a_pb2 import ( # type: ignore[import-untyped] | ||
| AgentCapabilities, | ||
| AgentCard, | ||
| AgentInterface, | ||
| AgentSkill, | ||
| ) | ||
|
|
||
| from agents.handoffs import Handoff | ||
| from agents.tool import FunctionTool | ||
|
|
||
| # -- description -------------------------------------------------------- | ||
| description = agent.handoff_description or "" | ||
| if not description and agent.instructions: | ||
| if isinstance(agent.instructions, str): | ||
| # Truncate very long instructions for the card. | ||
| description = agent.instructions[:2000] | ||
| else: | ||
| description = "Dynamic instructions (callable)." | ||
|
|
||
| # -- skills ------------------------------------------------------------- | ||
| skills: list[AgentSkill] = [] | ||
|
|
||
| for tool in agent.tools: | ||
| if not isinstance(tool, FunctionTool): | ||
| continue | ||
| skill = AgentSkill( | ||
| id=tool.name, | ||
| name=tool.name, | ||
| description=tool.description[:2000], | ||
| tags=_tool_tags(tool), | ||
| input_modes=["text"], | ||
| output_modes=["text"], | ||
| ) | ||
| skills.append(skill) | ||
|
|
||
| for handoff in agent.handoffs: | ||
| if isinstance(handoff, Handoff): | ||
| skill = AgentSkill( | ||
| id=handoff.tool_name, | ||
| name=handoff.agent_name, | ||
| description=handoff.handoff_description or handoff.tool_description or "", | ||
| tags=["handoff"], | ||
| input_modes=["text"], | ||
| output_modes=["text"], | ||
| ) | ||
| skills.append(skill) | ||
|
|
||
| # -- capabilities ------------------------------------------------------- | ||
| if capabilities is None: | ||
| capabilities = AgentCapabilities( | ||
| streaming=True, | ||
| push_notifications=False, | ||
| ) | ||
|
|
||
| # -- interfaces --------------------------------------------------------- | ||
| if supported_interfaces is None: | ||
| supported_interfaces = [ | ||
| AgentInterface( | ||
| url=url, | ||
| protocol_binding="a2a-json-rpc", | ||
| protocol_version="1.0", | ||
| ), | ||
| ] | ||
|
|
||
| return AgentCard( | ||
| name=agent.name, | ||
| description=description, | ||
| version=version, | ||
| supported_interfaces=supported_interfaces, | ||
| default_input_modes=["text"], | ||
| default_output_modes=["text"], | ||
| capabilities=capabilities, | ||
| skills=skills, | ||
| provider=provider, | ||
| ) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _tool_tags(tool: Any) -> list[str]: | ||
| """Infer A2A skill tags from tool metadata.""" | ||
| tags = ["tool"] | ||
|
|
||
| # Tag known hosted tools | ||
| tool_type = type(tool).__name__ | ||
| type_to_tag: dict[str, str] = { | ||
| "FileSearchTool": "file-search", | ||
| "WebSearchTool": "web-search", | ||
| "CodeInterpreterTool": "code-interpreter", | ||
| "HostedMCPTool": "mcp", | ||
| "ShellTool": "shell", | ||
| } | ||
| if tag := type_to_tag.get(tool_type): | ||
| tags.append(tag) | ||
|
|
||
| # Include namespace if present | ||
| namespace = getattr(tool, "_tool_namespace", None) | ||
| if namespace: | ||
| tags.append(f"namespace:{namespace}") | ||
|
|
||
| return tags | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
supported_interfacesis omitted, every generated AgentCard advertisesa2a-json-rpc, but A2A 1.0 transport matching uses the standard binding strings such asJSONRPC,GRPC, andHTTP+JSON. Agents exposed with this helper will therefore publish cards that the defaultClientFactorycannot match, making the generated server card unusable by standard A2A clients unless callers manually override the interface.Useful? React with 👍 / 👎.