Building Your First Ai Chatbot With A2a Protocol And Langgraph: A Complete Guide
Ever wanted to build an AI chatbot that actually follows modern standards? One that other agents can talk to, not just humans? That's exactly what we're going to do today.
I'm going to walk you through creating a chatbot that speaks the A2A (Agent-to-Agent) protocol and uses LangGraph for its conversation logic. By the end of this guide, you'll have a working chatbot server and client, and you'll understand every piece of the puzzle.
Why This Matters
Before we dive in, let me explain why this is cool. Most chatbots are islands they only work in their own little ecosystem. But A2A is different. It's a standardized protocol that lets agents discover and talk to each other, kind of like how web browsers can talk to any website thanks to HTTP.
Think of it this way, instead of building yet another proprietary chatbot API, you're building something that plays nicely with the broader AI ecosystem.
What You're Building
Here's what we're creating:
- A chatbot server that responds to messages using GPT-4o-mini
- An A2A-compliant agent that publishes its capabilities via an "agent card"
- A client that can send messages and get responses
- LangGraph orchestration that manages the conversation flow
The best part? It's simpler than it sounds. Let's break it down.
What You'll Need
Before we start coding, make sure you have:
-
Python 3.12 or higher - Check with
python3 --version -
UV package manager - A modern Python package tool (install with
curl -LsSf https://astral.sh/uv/install.sh | sh) - An OpenAI API key - Grab one from platform.openai.com
Don't worry about installing packages globally UV handles all of that in an isolated environment.
Understanding the Key Concepts
What is A2A?
Think of A2A as a universal language for AI agents. It has three main pieces:
1. Agent Card - This is like a business card for your agent. It's a JSON file at /.well-known/agent-card.json that describes:
- name - What your agent is called (e.g., "LangGraph Chatbot")
- description - What it does (e.g., "Simple chatbot powered by LangGraph")
- url - Where it lives (e.g., "http://localhost:9999/")
- default_input_modes - What input it accepts (e.g., ["text"])
- default_output_modes - What output it produces (e.g., ["text"])
- capabilities - Special features like streaming support
- skills - A list of what your agent can do, with examples
Each skill includes:
- id - Unique identifier (e.g., "chat")
- name - Display name (e.g., "Chat")
- description - What this skill does
- tags - Keywords for discovery (e.g., ["chatbot", "langgraph"])
- examples - Sample prompts (e.g., "Tell me a joke")
Clients fetch this card first to discover what your agent can do and how to interact with it.
2. Message Exchange - The protocol defines how to send messages back and forth. A client sends a message, and your agent responds. Simple as that.
3. Agent Executor - This is where the magic happens it's the bridge between the A2A protocol and your actual agent logic.
Understanding the Agent Executor
The Agent Executor is the core component that processes incoming requests and generates responses. Think of it as the translator between the A2A protocol layer and your business logic (in our case, the LangGraph chatbot).
The A2A SDK provides an abstract base class AgentExecutor that you implement with two key methods:
execute(context, event_queue) This is the main workhorse:
-
context (RequestContext): Contains everything about the incoming request—the user's message, task ID, context ID, and more. You can call
context.get_user_input()to get the user's text. - event_queue (EventQueue): This is how you send responses back. Instead of directly writing HTTP responses, you enqueue A2A objects (like Messages) onto this queue, and the framework handles delivering them to the client.
The flow looks like this:
- User sends a message → A2A framework receives it
- Framework creates a RequestContext and EventQueue
- Calls your
execute(context, event_queue)method - You read
context.get_user_input(), process it (call LangGraph) - You enqueue the response:
event_queue.enqueue_event(new_agent_text_message(reply)) - Framework drains the queue and sends responses back to the client
cancel(context, event_queue) - Handles cancellation requests:
- For long-running tasks, clients can request cancellation
- Our simple chatbot doesn't support this, so we raise
NotImplementedError - Production agents might track in flight tasks and abort them when cancel is called
Here's a simple example structure:
class MyAgentExecutor(AgentExecutor):
async def execute(self, context: RequestContext, event_queue: EventQueue):
# 1. Get user input
user_input = context.get_user_input()
# 2. Process it (your business logic)
result = await my_agent.process(user_input)
# 3. Send response back
await event_queue.enqueue_event(new_agent_text_message(result))
async def cancel(self, context: RequestContext, event_queue: EventQueue):
raise NotImplementedError("cancellation not supported")
The beauty of this design is separation of concerns: the A2A framework handles all the protocol details (JSON-RPC, HTTP, streaming), while your executor focuses purely on the agent logic.
What is LangGraph?
LangGraph is a framework for building stateful AI agents as graphs. Here's the mental model:
- State: A dictionary that flows through your agent (in our case, it holds the conversation messages)
- Nodes: Functions that do work (we have one node that calls the LLM)
- Edges: Connections between nodes (we connect START → model → END)
-
Reducers: Rules for how to merge updates (we use
add_messagesso new messages get appended, not replaced)
Our agent is beautifully simple: it takes a message, sends it to GPT-4o-mini, and returns the response. No complicated branching, no tools—just pure conversation.
Step 1: Setting Up Your Project
Let's create a clean workspace:
mkdir LG_A2A
cd LG_A2A
uv venv
uv init
Here's what just happened:
-
uv venvcreated a virtual environment in.venv -
uv initset up a Python project withpyproject.toml
You don't need to "activate" the virtual environment UV handles that automatically when you run things with uv run.
Step 2: Installing Dependencies
Open pyproject.toml and add these dependencies:
[project]
name = "lg-a2a"
version = "0.1.0"
description = "A2A + LangGraph chatbot"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"a2a-sdk[http-server]>=0.3",
"langgraph>=0.2",
"langchain-core>=0.3",
"langchain-openai>=0.2",
"uvicorn[standard]>=0.30",
]
Each package has a job:
- a2a-sdk: Implements the A2A protocol (server, client, everything)
- langgraph: Our agent framework
- langchain-core: Base types for messages
- langchain-openai: Connects to OpenAI's API
- uvicorn: Runs our web server
Install everything:
uv sync
Step 3: Building the LangGraph Agent
Create agent.py. This is the brain of your chatbot:
"""LangGraph chatbot agent: simple state + model node, no tools."""
from typing import Annotated
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
class ChatState(TypedDict):
"""State for the chatbot graph: messages list with add_messages reducer."""
messages: Annotated[list[BaseMessage], add_messages]
def model_node(state: ChatState) -> dict:
"""Call the LLM with current messages and return the assistant reply."""
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
response = model.invoke(state["messages"])
return {"messages": [response]}
def build_chatbot_agent():
"""Build and compile the LangGraph chatbot (START -> model -> END)."""
builder = StateGraph(ChatState)
builder.add_node("model", model_node)
builder.add_edge(START, "model")
builder.add_edge("model", END)
return builder.compile()
chatbot_agent = build_chatbot_agent()
What's happening here?
- ChatState defines what data flows through the graph just a list of messages
- model_node is where the magic happens: it calls GPT-4o-mini with the conversation history
- build_chatbot_agent wires everything together: START → call the model → END
- The
add_messagesreducer means each new message gets appended to the list, not replaced
Think of it as a simple pipeline: message comes in → LLM processes it → response goes out.
Step 4: Connecting to A2A with the Agent Executor
Create agent_executor.py. This is the crucial bridge between A2A and LangGraph:
"""A2A Agent Executor that runs the LangGraph chatbot for each user message."""
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.utils import new_agent_text_message
from langchain_core.messages import HumanMessage
from agent import chatbot_agent
class ChatbotAgentExecutor(AgentExecutor):
"""Executes the LangGraph chatbot: gets user input from A2A context, invokes agent, enqueues reply."""
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
# Step 1: Get the user's message from the A2A context
user_input = context.get_user_input()
# Step 2: Validate the input
if not user_input.strip():
await event_queue.enqueue_event(
new_agent_text_message("Please send a non-empty message.")
)
return
# Step 3: Run it through LangGraph
messages = [HumanMessage(content=user_input)]
result = chatbot_agent.invoke({"messages": messages})
# Step 4: Extract the reply from the LangGraph result
last = result["messages"][-1]
reply = last.content if hasattr(last, "content") else str(last)
# Step 5: Send it back to the client via the event queue
await event_queue.enqueue_event(new_agent_text_message(reply))
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
# Our simple agent doesn't support cancellation
raise NotImplementedError("cancel not supported")
Breaking it down:
This executor implements the AgentExecutor interface. Here's what happens when a message arrives:
-
Extract input:
context.get_user_input()gives us the user's text from the A2A message - Validate: We check if it's empty and send a helpful error if needed
-
Process: We wrap the input in a
HumanMessageand invoke our LangGraph agent - Extract output: We get the last message (the AI's response) from the result
-
Enqueue response: We use
new_agent_text_message(reply)to create an A2A Message and push it onto the event queue
The event queue is key: you don't write HTTP responses yourself. The A2A framework drains this queue and handles all the protocol details—whether it's a single response for message/send or a stream of events for message/stream.
The cancel method would handle task cancellation in a production agent, but our simple chatbot doesn't support long running tasks, so we just raise NotImplementedError.
Step 5: Creating the Server
Create server.py. This sets up the HTTP server:
"""A2A server: Agent Card, request handler, and Starlette app."""
from dotenv import load_dotenv
load_dotenv() # Load environment variables FIRST
import uvicorn
from starlette.responses import HTMLResponse
from starlette.routing import Route
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from agent_executor import ChatbotAgentExecutor
ROOT_HTML = """
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>LangGraph Chatbot (A2A)</title></head>
<body>
<h1>LangGraph Chatbot (A2A)</h1>
<p>Server is running. This agent speaks the <a href="https://a2a-protocol.org/">A2A protocol</a>.</p>
<ul>
<li><a href="/.well-known/agent-card.json">Agent card</a> (JSON)</li>
<li>Send messages via A2A <code>message/send</code> or <code>message/stream</code>.</li>
</ul>
</body>
</html>
"""
async def root_page(request):
return HTMLResponse(ROOT_HTML)
def main() -> None:
# Define what your agent can do
skill = AgentSkill(
id="chat",
name="Chat",
description="Chat with the LangGraph chatbot. Send any text message.",
tags=["chat", "chatbot", "langgraph"],
examples=["Hello", "What is the weather?", "Tell me a joke"],
)
# Create the agent card (this is what clients see)
agent_card = AgentCard(
name="LangGraph Chatbot (A2A)",
description="Simple chatbot powered by LangGraph and the A2A protocol.",
url="http://localhost:9999/",
version="0.1.0",
default_input_modes=["text"],
default_output_modes=["text"],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
)
# Wire up the request handler with our executor
request_handler = DefaultRequestHandler(
agent_executor=ChatbotAgentExecutor(),
task_store=InMemoryTaskStore(),
)
# Build the app
a2a_app = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
)
app = a2a_app.build()
# Add a friendly homepage
app.routes.insert(0, Route("/", root_page, methods=["GET"]))
# Start the server
uvicorn.run(app, host="0.0.0.0", port=9999)
if __name__ == "__main__":
main()
Key points:
-
load_dotenv()at the top loads your API key from.env - The
AgentCardis your agent's public profile it tells clients your agent's name, what it can do, what input/output it supports, and provides example prompts - The
AgentSkilldescribes the specific capability (in our case, "chat") -
DefaultRequestHandlerwires our executor to the A2A protocol—it receives requests, calls our executor, and handles responses -
InMemoryTaskStoremanages task state (useful for streaming and long-running operations) - We add a simple HTML homepage so visiting
http://localhost:9999/is user-friendly
Step 6: Building the Client
Create client.py to talk to your server:
"""A2A client: fetches agent card, sends messages to the LangGraph chatbot server."""
import asyncio
import sys
from uuid import uuid4
import httpx
from a2a.client import A2ACardResolver, A2AClient
from a2a.client.helpers import create_text_message_object
from a2a.types import MessageSendParams, SendMessageRequest
BASE_URL = "http://localhost:9999"
def extract_text_from_message(message) -> str:
"""Get plain text from an A2A Message (agent reply)."""
parts = []
for part in message.parts:
if hasattr(part, "root") and hasattr(part.root, "text"):
parts.append(part.root.text)
elif hasattr(part, "text"):
parts.append(part.text)
return "\n".join(parts) if parts else str(message)
async def send_one(client: A2AClient, text: str) -> str:
"""Send a single message and return the agent's reply text."""
user_message = create_text_message_object(content=text)
params = MessageSendParams(message=user_message)
request = SendMessageRequest(id=str(uuid4()), params=params)
response = await client.send_message(request)
if hasattr(response.root, "error") and response.root.error is not None:
return f"Error: {response.root.error}"
result = response.root.result
if hasattr(result, "parts"):
return extract_text_from_message(result)
return str(result)
async def run_client():
async with httpx.AsyncClient(timeout=60.0) as httpx_client:
# Fetch the agent card
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=BASE_URL)
card = await resolver.get_agent_card()
client = A2AClient(httpx_client=httpx_client, agent_card=card)
# One-shot mode: send a single message from command line
if len(sys.argv) > 1:
text = " ".join(sys.argv[1:])
reply = await send_one(client, text)
print(reply)
return
# Interactive mode: chat in a loop
print(f"Connected to {card.name} at {BASE_URL}")
print("Type a message and press Enter (empty line or Ctrl+C to quit).\n")
while True:
try:
line = input("You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
break
if not line:
break
reply = await send_one(client, text)
print(f"Agent: {reply}\n")
def main():
asyncio.run(run_client())
if __name__ == "__main__":
main()
This client does two things:
-
One-shot mode: Run
uv run client.py "Hello"to send one message -
Interactive mode: Run
uv run client.pyto chat back and forth
Step 7: Adding Your API Key
Create a .env file in your project root:
OPENAI_API_KEY=sk-proj-your-actual-key-here
Replace sk-proj-your-actual-key-here with your real OpenAI API key.
Important: Add .env to your .gitignore so you never commit your secrets:
.env
Create a .env.example as a template for others:
OPENAI_API_KEY=your_openai_api_key_here
Step 8: Running Your Chatbot
Start the server
uv run server.py
You should see:
INFO: Uvicorn running on http://0.0.0.0:9999
Test in your browser
Open http://localhost:9999/ and you'll see your chatbot's homepage with a link to the agent card.
Click the "Agent card" link to see the JSON that describes your agent's capabilities.
Try the client
In another terminal, send a one-off message:
uv run client.py "Tell me a joke about Python"
Or start an interactive chat:
uv run client.py
Type messages at the You: prompt and watch the agent respond!
Testing the Agent & Result or output
Agent Card
Client and A2A connected
What You Just Built
Let's recap what you accomplished:
✅ A fully functional AI chatbot server
✅ An A2A-compliant agent that publishes its capabilities
✅ A LangGraph-powered conversation engine
✅ An Agent Executor that bridges A2A and your business logic
✅ Both one-shot and interactive clients
✅ Proper secret management with .env
Where to Go From Here
This is just the beginning. Here are some ideas to extend your chatbot:
Add conversation history: Modify the executor to maintain context across messages using the A2A context_id.
Add tools: Give your agent capabilities like web search or calculations by adding tool nodes to the LangGraph graph.
Stream responses: Implement message/stream to send responses as they're generated instead of all at once.
Add memory: Use LangGraph's checkpointers to remember past conversations.
Handle cancellation: Implement the cancel method to abort long-running tasks when clients request it.
Deploy it: Put your server on a real domain so other A2A agents can discover and talk to it.
Final Thoughts
What I love about this architecture is how clean the separation of concerns is. The A2A framework handles all the protocol complexity (JSON-RPC, HTTP, streaming, agent cards), the Agent Executor acts as a simple bridge, and LangGraph handles the agent logic. Each piece does one thing well.
You're not locked into a vendor's ecosystem. You're building on open standards. Your agent can talk to other A2A agents, and vice versa. That's the future of AI agents not walled gardens, but an open web of collaborating intelligences.
Now go build something cool with it! And if you get stuck, remember: the agent card at /.well-known/agent-card.json is your friend it tells you exactly what your agent can do and how to talk to it.
Happy coding!
Thanks
Sreeni Ramadorai
Popular Products
-
Devil Horn Headband$25.99$11.78 -
WiFi Smart Video Doorbell Camera with...$61.56$30.78 -
Smart GPS Waterproof Mini Pet Tracker$59.56$29.78 -
Unisex Adjustable Back Posture Corrector$71.56$35.78 -
Smart Bluetooth Aroma Diffuser$585.56$292.87