Build a Claude Chatbot

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 191 of 200

The Goal

This project builds the core of a chatbot: a component that remembers the conversation, sends the history with each new message, keeps the history within a limit, and records the transcript. It is the heart of every chat product, whatever its interface. The chat window, a web page, or a messaging app are all presentation layers around this component.

You will build the component as a class with one method that accepts a user message and returns the reply. The class receives its client from outside, so the same code runs against the live API or against a stand-in that the tests and the demonstration use. Nothing here requires a key to run the demonstration.


What You Will Build

A ChatSession object that:

  1. keeps the conversation as a list of messages, in the order they were exchanged,
  2. sends the system prompt and the recent history with every request,
  3. keeps only the most recent turns, so the request stays within a size you choose,
  4. saves the full transcript, so the conversation can be stored and resumed.

The history is the only memory the model has. Each request must carry the context it needs, which is why the session sends the history every time (Lesson 24), and why the trimming must be done carefully.


Project Structure

chatbot/
├── chat_session.py    # the ChatSession class
└── demo.py            # a stand-in client, and a short conversation

Step 1: The Session

The session holds the history, and its send method performs the exchange. The history alternates between user and assistant messages, which the API requires, so the code must keep that alternation intact even when it trims:

"""chat_session.py: a conversation that keeps its history and sends it with each request."""

import json


class ChatSession:
    def __init__(self, client, system: str, model: str = "claude-sonnet-5-5",
                 max_turns: int = 10, max_tokens: int = 512):
        self.client = client
        self.system = system
        self.model = model
        self.max_turns = max_turns
        self.max_tokens = max_tokens
        self.history: list[dict] = []

    def send(self, text: str) -> str:
        self.history.append({"role": "user", "content": text})
        response = self.client.messages.create(
            model=self.model,
            max_tokens=self.max_tokens,
            system=self.system,
            messages=self._recent(),
        )
        reply = "".join(block.text for block in response.content if block.type == "text")
        self.history.append({"role": "assistant", "content": reply})
        return reply

    def _recent(self) -> list[dict]:
        start = max(0, len(self.history) - 2 * self.max_turns)
        if start % 2:
            start += 1                      # Begin on a user message, never an assistant message
        return self.history[start:]

    def transcript(self) -> str:
        return json.dumps(self.history, ensure_ascii=False, indent=2)

How It Works

  1. The history grows by two messages per exchange, one from the user and one from the assistant. Storing the full reply, not only its text, keeps the history in the form the API expects.
  2. _recent() selects the messages to send. It keeps the last 2 × max_turns messages, which is the number of messages in the most recent turns.
  3. The start index is forced to be even. The history begins with a user message at index zero, and every user message sits at an even index. A start at an odd index would begin on an assistant message, which the API rejects, so the code moves the start forward by one when needed.
  4. The system prompt is sent separately, on every request, so trimming the history never removes the instructions.
  5. The transcript is the full history, not the trimmed view, so the record of the conversation is complete even though the model saw only the recent part.

Step 2: A Stand-In Client

To run the project without a key, the demonstration uses a client that records what it was asked and returns a predictable reply. It has the same shape as the real client's messages method, so the session cannot tell the difference:

"""demo.py: run a short conversation against a stand-in client."""

from types import SimpleNamespace

from chat_session import ChatSession


class EchoClient:
    def __init__(self):
        self.calls: list[list[dict]] = []
        self.messages = self

    def create(self, **request):
        self.calls.append(request["messages"])
        last = request["messages"][-1]["content"]
        text = f"reply to '{last}'"
        return SimpleNamespace(content=[SimpleNamespace(type="text", text=text)])


if __name__ == "__main__":
    client = EchoClient()
    session = ChatSession(client, system="You are a helpful tutor.", max_turns=2)
    for number, message in enumerate(["Hi, I am Priya", "What is a loop?", "What is my name?"], start=1):
        reply = session.send(message)
        sent = client.calls[-1]
        print(f"turn {number} sent {len(sent)} messages: {reply}")
    print("saved messages:", len(session.history))

Output:

turn 1 sent 1 messages: reply to 'Hi, I am Priya'
turn 2 sent 3 messages: reply to 'What is a loop?'
turn 3 sent 3 messages: reply to 'What is my name?'
saved messages: 6

How It Works

  1. EchoClient mirrors the real client's shape. Its messages attribute has a create method that accepts the same keyword arguments, so the session calls it exactly as it would call the live API.
  2. The first turn sends one message, because there is no history yet.
  3. The second turn sends three: the first exchange and the new question, so the model can see what was said before.
  4. The third turn still sends three, because the limit of two turns is reached. The first exchange has been dropped from the request, though it remains in the transcript, which the last line confirms with six saved messages.
  5. The stand-in's reply shows what the model received. The reply quotes the last user message, and the count shows how much history went with it.

Going Live

To use the real API, replace the stand-in with the SDK client, and keep everything else the same:

from anthropic import Anthropic

session = ChatSession(Anthropic(), system="You are a helpful tutor.")
print(session.send("What is a loop?"))

The Anthropic() client reads the key from the environment (Lesson 172), so the code itself contains no secret. The session does not change, because the call signature is the same.


Where the Design Can Grow

The class deliberately does less than a production chatbot. A production version adds streaming (Lessons 51 to 60), persistence of the transcript to a database (Lesson 62), error handling around the call (Lessons 141 to 150), and a token-based limit in place of a turn count (Lesson 158). Each addition is a change inside send, so the structure built here remains the base.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Build a Claude Chatbot and get answers drawn from it.

Signed-in readers only.