Friday, April 18, 2025

Bridging the Gap: An Introduction to Google’s Agent-to-Agent (A2A) Protocol with Python Examples

 In the ever-evolving landscape of AI agents, seamless communication and interoperability are paramount. Imagine a scenario where one specialized agent needs to leverage the output of another to fulfill a user’s request. Without a standardized way for these agents to interact, we often find ourselves building intricate and brittle integration layers.

Consider this:

Agent 1: A service connected to a local Spring Boot application that provides the current temperature of a specific location.

Agent 2: An OpenAI-powered agent that takes the temperature as input and suggests activities suitable for that weather.

The challenge arises because the raw output of Agent 1 isn’t directly consumable by Agent 2. We’d typically need to manually transform the data into a format OpenAI understands. This becomes a significant headache when you need to swap out services or when underlying models are updated, forcing you to rewrite substantial portions of your communication logic. Remember the pain when OpenAI released a new model or when you wanted to experiment with Anthropic’s Claude? It often meant significant code rewrites in the communication layer.

To address this very problem, Google introduced the Agent-to-Agent (A2A) protocol. This protocol aims to standardize the way AI agents communicate, fostering a more flexible and robust ecosystem. Let’s dive into how we can use the python-a2a library to implement this.

Step 1: Installation

First things first, let’s install the necessary Python library:

Bash

1
pip install python-a2a

Step 2: Creating an Agent Server (Addition Example)

Let’s build a simple agent that exposes an “add” method. This agent will receive a text message containing two comma-separated numbers and return their sum.

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# --- Modified AddAgent Server Code ---
from python_a2a import A2AServer, Message, TextContent, MessageRole, run_server
import logging
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
class AddAgent(A2AServer):
    """
    An agent that expects a single text message containing two numbers
    separated by a comma (e.g., "5,2") and returns their sum.
    """
 
    # MODIFIED: handle_message now accepts ONE message argument
    def handle_message(self, message: Message):
        logging.info(f"Received message: {message}")
 
        # Check if it's a text message
        if not (hasattr(message, 'content') and message.content.type == "text"):
            warning_msg = "Error: Input must be a text message."
            logging.warning(warning_msg)
            return Message(
                content=TextContent(text=warning_msg),
                role=MessageRole.AGENT,
                parent_message_id=getattr(message, 'message_id', None),
                conversation_id=getattr(message, 'conversation_id', None)
            )
 
        # Get the text and try to parse it
        text_input = message.content.text
        logging.info(f"Attempting to parse text: '{text_input}'")
 
        try:
            # Split the text by a comma (or another delimiter you choose)
            parts = text_input.split(',')
            if len(parts) != 2:
                raise ValueError("Input text must contain exactly two numbers separated by a comma.")
 
            # Attempt conversion to float
            num1 = float(parts[0].strip()) # Use strip() to remove leading/trailing whitespace
            num2 = float(parts[1].strip())
 
            # Perform the addition
            result = num1 + num2
            response_text = f"The sum of {num1} and {num2} is: {result}"
            logging.info(f"Calculation successful: {response_text}")
 
            # Return the result message
            return Message(
                content=TextContent(text=response_text),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
        except ValueError as e:
            # Handle cases where splitting fails or text cannot be converted
            error_msg = f"Error processing input '{text_input}': {e}"
            logging.error(error_msg)
            return Message(
                content=TextContent(text=error_msg),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
        except Exception as e:
            error_msg = f"An unexpected error occurred: {e}"
            logging.exception(error_msg) # Log full traceback
            return Message(
                content=TextContent(text="An internal server error occurred."),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
# Run the server (no changes needed here)
if __name__ == "__main__":
    agent = AddAgent()
    print("Starting AddAgent server...")
    # Now the handle_message signature matches what run_server expects
    run_server(agent, host="localhost", port=5000)

Save this code as a2a_add_server.py and run it. You should see the server starting on http://localhost:5000/a2a.

Step 3: Creating Another Agent Server (Subtraction Example)

Let’s create another agent, this time for subtraction, running on a different port.

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# --- Modified AddAgent Server Code ---
from python_a2a import A2AServer, Message, TextContent, MessageRole, run_server
import logging
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
class SubstractAgent(A2AServer):
    """
    An agent that expects a single text message containing two numbers
    separated by a comma (e.g., "5,2") and returns their Substraction.
    """
 
    # MODIFIED: handle_message now accepts ONE message argument
    def handle_message(self, message: Message):
        logging.info(f"Received message: {message}")
 
        # Check if it's a text message
        if not (hasattr(message, 'content') and message.content.type == "text"):
            warning_msg = "Error: Input must be a text message."
            logging.warning(warning_msg)
            return Message(
                content=TextContent(text=warning_msg),
                role=MessageRole.AGENT,
                parent_message_id=getattr(message, 'message_id', None),
                conversation_id=getattr(message, 'conversation_id', None)
            )
 
        # Get the text and try to parse it
        text_input = message.content.text
        logging.info(f"Attempting to parse text: '{text_input}'")
 
        try:
            # Split the text by a comma (or another delimiter you choose)
            parts = text_input.split(',')
            if len(parts) != 2:
                raise ValueError("Input text must contain exactly two numbers separated by a comma.")
 
            # Attempt conversion to float
            num1 = float(parts[0].strip()) # Use strip() to remove leading/trailing whitespace
            num2 = float(parts[1].strip())
 
            # Perform the addition
            result = num1 - num2
            response_text = f"The Substraction of {num1} and {num2} is: {result}"
            logging.info(f"Calculation successful: {response_text}")
 
            # Return the result message
            return Message(
                content=TextContent(text=response_text),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
        except ValueError as e:
            # Handle cases where splitting fails or text cannot be converted
            error_msg = f"Error processing input '{text_input}': {e}"
            logging.error(error_msg)
            return Message(
                content=TextContent(text=error_msg),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
        except Exception as e:
            error_msg = f"An unexpected error occurred: {e}"
            logging.exception(error_msg) # Log full traceback
            return Message(
                content=TextContent(text="An internal server error occurred."),
                role=MessageRole.AGENT,
                parent_message_id=message.message_id,
                conversation_id=message.conversation_id
            )
 
# Run the server (no changes needed here)
if __name__ == "__main__":
    agent = SubstractAgent()
    print("Starting SubstractAgent server...")
    # Now the handle_message signature matches what run_server expects
    run_server(agent, host="localhost", port=5001)

Save this as a2a_substract_server.py and run it in a separate terminal. This server will start on http://localhost:5001/a2a.

Step 4: Creating an A2A Client

Now, let’s create a client that can communicate with both of these agents using the A2A protocol.

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from python_a2a import A2AClient, Message, TextContent, MessageRole
 
# Create a client to talk to our addition agent
client_add = A2AClient("http://localhost:5000/a2a")
# Send a message
message_add = Message(
    content=TextContent(text="5,2"),
    role=MessageRole.USER
)
response_add = client_add.send_message(message_add)
# Print the response
print(f"Add Agent says: {response_add.content.text}")
 
 
# Create a client to talk to our subtraction agent
client_sub = A2AClient("http://localhost:5001/a2a")
# Send a message
message_sub = Message(
    content=TextContent(text="5,2"),
    role=MessageRole.USER
)
response_sub = client_sub.send_message(message_sub)
# Print the response
print(f"Sunstract Agent says: {response_sub.content.text}")

Save this code as a2a_client_agent_add_substract.py and execute it. You should see the following output:

1
2
Add Agent says: The sum of 5.0 and 2.0 is: 7.0
Sunstract Agent says: The Substraction of 5.0 and 2.0 is: 3.0

Conclusion

This simple example demonstrates the fundamental principles of the A2A protocol using the python-a2a library. By standardizing the message format and communication layer, A2A paves the way for more modular, maintainable, and interoperable AI agent systems. When you need to swap out an underlying service or integrate with a new model, the A2A protocol helps abstract away the low-level communication details, reducing the need for extensive code modifications.

You can find the complete code for this example on GitHub: https://github.com/shdhumale/A2AServerClient.git

As the field of AI agents continues to mature, protocols like A2A will be crucial in building complex and collaborative AI ecosystems. By embracing these standards, we can move towards a future where AI agents can seamlessly work together to solve intricate problems.

No comments: