Join our FREE personalized newsletter for news, trends, and insights that matter to everyone in America

Newsletter
New

Building Your First Ai Chatbot With A2a Protocol And Langgraph: A Complete Guide

Card image cap

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:

  1. Python 3.12 or higher - Check with python3 --version
  2. UV package manager - A modern Python package tool (install with curl -LsSf https://astral.sh/uv/install.sh | sh)
  3. 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:

  1. User sends a message → A2A framework receives it
  2. Framework creates a RequestContext and EventQueue
  3. Calls your execute(context, event_queue) method
  4. You read context.get_user_input(), process it (call LangGraph)
  5. You enqueue the response: event_queue.enqueue_event(new_agent_text_message(reply))
  6. 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_messages so 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 venv created a virtual environment in .venv
  • uv init set up a Python project with pyproject.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?

  1. ChatState defines what data flows through the graph just a list of messages
  2. model_node is where the magic happens: it calls GPT-4o-mini with the conversation history
  3. build_chatbot_agent wires everything together: START → call the model → END
  4. The add_messages reducer 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:

  1. Extract input: context.get_user_input() gives us the user's text from the A2A message
  2. Validate: We check if it's empty and send a helpful error if needed
  3. Process: We wrap the input in a HumanMessage and invoke our LangGraph agent
  4. Extract output: We get the last message (the AI's response) from the result
  5. 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 AgentCard is 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 AgentSkill describes the specific capability (in our case, "chat")
  • DefaultRequestHandler wires our executor to the A2A protocol—it receives requests, calls our executor, and handles responses
  • InMemoryTaskStore manages 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:

  1. One-shot mode: Run uv run client.py "Hello" to send one message
  2. Interactive mode: Run uv run client.py to 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