<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ claude-api - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ claude-api - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 23 Oct 2026 15:43:42 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/claude-api/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Reliable AI Assistant with the Claude API ]]>
                </title>
                <description>
                    <![CDATA[ Large language models can answer questions, summarise documents, write code, and interact with external systems. But building a reliable AI application requires more than sending a prompt and displayi ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-reliable-ai-assistant-with-the-claude-api/</link>
                <guid isPermaLink="false">6abbc1526882db86928eaf11</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Prompt Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude-api ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chidozie Managwu ]]>
                </dc:creator>
                <pubDate>Tue, 29 Sep 2026 13:46:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c4c96758-54f5-4b88-9f00-639ecff705f9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Large language models can answer questions, summarise documents, write code, and interact with external systems. But building a reliable AI application requires more than sending a prompt and displaying the response.</p>
<p>A production-ready application must manage conversation history, provide relevant context, use tools safely, handle different response types, and evaluate whether the generated output is useful.</p>
<p>In this tutorial, we’ll build <strong>ShopHelper</strong>, a customer-support assistant for an imaginary online shop. By the end, ShopHelper will be able to:</p>
<ul>
<li><p>Answer general questions in a consistent tone</p>
</li>
<li><p>Remember what a customer said earlier</p>
</li>
<li><p>Look up order statuses by calling a function in your code</p>
</li>
<li><p>Handle Claude’s multi-block responses safely</p>
</li>
<li><p>Process support tickets using workflows</p>
</li>
<li><p>Evaluate whether prompt changes improve results</p>
</li>
</ul>
<p>Each section adds one piece, so you can follow along in your own editor.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-project-and-keep-your-api-key-secure">How to Set Up the Project and Keep Your API Key Secure</a></p>
</li>
<li><p><a href="#heading-how-to-make-your-first-request">How to Make Your First Request</a></p>
</li>
<li><p><a href="#heading-how-to-manage-conversation-history">How to Manage Conversation History</a></p>
</li>
<li><p><a href="#heading-how-to-structure-prompts-with-clear-boundaries">How to Structure Prompts with Clear Boundaries</a></p>
</li>
<li><p><a href="#heading-how-to-use-a-system-prompt">How to Use a System Prompt</a></p>
</li>
<li><p><a href="#heading-how-to-add-tools">How to Add Tools</a></p>
</li>
<li><p><a href="#heading-how-to-handle-a-tool-use-response">How to Handle a Tool-Use Response</a></p>
</li>
<li><p><a href="#heading-claude-responses-can-contain-multiple-blocks">Claude Responses Can Contain Multiple Blocks</a></p>
</li>
<li><p><a href="#heading-workflows-vs-agents">Workflows vs Agents</a></p>
</li>
<li><p><a href="#heading-chaining-parallelisation-routing-and-evaluator-optimizer">Chaining, Parallelisation, Routing and Evaluator-Optimizer</a></p>
</li>
<li><p><a href="#heading-how-to-evaluate-prompt-quality">How to Evaluate Prompt Quality</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should have:</p>
<ul>
<li><p>Basic Python knowledge</p>
</li>
<li><p>Python 3.9 or later</p>
</li>
<li><p>An Anthropic API key</p>
</li>
<li><p>Familiarity with functions and JSON</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-the-project-and-keep-your-api-key-secure">How to Set Up the Project and Keep Your API Key Secure</h2>
<p>Create a virtual environment and install the Anthropic Python SDK:</p>
<pre><code class="language-bash">python -m venv .venv
source .venv/bin/activate
pip install anthropic python-dotenv
</code></pre>
<p>On Windows:</p>
<pre><code class="language-bash">.venv\Scripts\activate
</code></pre>
<p>Create a <code>.env</code> file:</p>
<pre><code class="language-text">ANTHROPIC_API_KEY=your_api_key_here
</code></pre>
<p>An API key is a secret credential. Never place it in browser JavaScript, mobile-app code, or client-side configuration. Never commit it to a repository:</p>
<pre><code class="language-bash">echo ".env" &gt;&gt; .gitignore
</code></pre>
<p>If you add a web interface later, keep the key on your backend:</p>
<pre><code class="language-text">Browser → Your backend → Claude API
</code></pre>
<p>Create <code>app.py</code>:</p>
<pre><code class="language-python">import os

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()

MODEL = "claude-sonnet-5"

client = Anthropic(
    api_key=os.environ["ANTHROPIC_API_KEY"]
)
</code></pre>
<p><code>load_dotenv()</code> loads the value from <code>.env</code>. The <code>MODEL</code> constant means you only need to change the model name in one place. Confirm that the model identifier is available to your account before running the example.</p>
<h2 id="heading-how-to-make-your-first-request">How to Make Your First Request</h2>
<pre><code class="language-python">response = client.messages.create(
    model=MODEL,
    max_tokens=500,
    messages=[
        {
            "role": "user",
            "content": "Explain what an API is in simple terms."
        }
    ],
)

answer = "".join(
    block.text
    for block in response.content
    if block.type == "text"
)

print(answer)
</code></pre>
<p>A request contains three important parts:</p>
<ul>
<li><p><code>model</code> selects the Claude model that handles the request. Models can differ in capability, speed, and cost.</p>
</li>
<li><p><code>max_tokens</code> limits the maximum amount of text Claude can generate. A smaller value can reduce latency, but Claude may stop before completing its answer.</p>
</li>
<li><p><code>messages</code> contains the conversation. Each message has a <code>role</code> and <code>content</code>. The role is usually <code>user</code> or <code>assistant</code>.</p>
</li>
</ul>
<p>For example, a one-off request contains one user message. A multi-turn conversation contains earlier user and assistant messages.</p>
<p>Claude returns <code>response.content</code>, which is a list of typed content blocks. Common blocks include:</p>
<table>
<thead>
<tr>
<th>Block type</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td><code>text</code></td>
<td>Generated text</td>
</tr>
<tr>
<td><code>tool_use</code></td>
<td>A request for your application to call a tool</td>
</tr>
<tr>
<td><code>thinking</code></td>
<td>Reasoning content when enabled</td>
</tr>
</tbody></table>
<p>The example collects text blocks instead of assuming <code>response.content[0]</code> is always text.</p>
<p>You can inspect usage information for monitoring:</p>
<pre><code class="language-python">print(response.usage.input_tokens)
print(response.usage.output_tokens)
</code></pre>
<h2 id="heading-how-to-manage-conversation-history">How to Manage Conversation History</h2>
<p>Claude doesn't automatically remember separate API requests. Send relevant history with every request:</p>
<pre><code class="language-python">messages = [
    {
        "role": "user",
        "content": "What is your returns policy?"
    },
    {
        "role": "assistant",
        "content": "Items can be returned within 30 days."
    },
    {
        "role": "user",
        "content": "How long do I have?"
    },
]

response = client.messages.create(
    model=MODEL,
    max_tokens=300,
    messages=messages,
)
</code></pre>
<p>The assistant message records Claude’s earlier answer, allowing the final question to be interpreted in context.</p>
<p>A simple chat function can maintain the history:</p>
<pre><code class="language-python">def chat(history, user_text):
    history.append({
        "role": "user",
        "content": user_text,
    })

    response = client.messages.create(
        model=MODEL,
        max_tokens=500,
        messages=history,
    )

    reply = "".join(
        block.text
        for block in response.content
        if block.type == "text"
    )

    history.append({
        "role": "assistant",
        "content": reply,
    })

    return reply


history = []

print(chat(history, "What is your returns policy?"))
print(chat(history, "How long do I have?"))
</code></pre>
<p>Each call adds the new user message, sends the complete history, and stores Claude’s response for the next turn. In production, store histories by customer or session ID.</p>
<h3 id="heading-how-to-manage-history-as-it-grows">How to Manage History as it Grows</h3>
<p>Unlimited history increases input size and may make it harder for Claude to focus. One option is to retain only recent messages:</p>
<pre><code class="language-python">def trim_history(history, max_messages=10):
    trimmed = history[-max_messages:]

    while trimmed and trimmed[0]["role"] != "user":
        trimmed.pop(0)

    return trimmed
</code></pre>
<p>Another option is to summarise older turns while keeping recent messages:</p>
<pre><code class="language-python">def summarise_history(history, keep_last=6):
    old = history[:-keep_last]
    recent = history[-keep_last:]

    transcript = "\n".join(
        f"{message['role']}: {message['content']}"
        for message in old
    )

    response = client.messages.create(
        model=MODEL,
        max_tokens=250,
        messages=[{
            "role": "user",
            "content": (
                "Summarise this conversation in under 100 words. "
                "Keep order numbers and unresolved issues.\n\n"
                f"&lt;conversation&gt;{transcript}&lt;/conversation&gt;"
            ),
        }],
    )

    summary = "".join(
        block.text
        for block in response.content
        if block.type == "text"
    )

    return summary, recent
</code></pre>
<p>Keep the summary as separate application state and include it as context in the next request. Don't insert it as an additional user message before <code>recent</code>, because that can create invalid consecutive user messages.</p>
<p>Sensitive information should also be redacted before storage or transmission:</p>
<pre><code class="language-python">import re

def redact(text):
    return re.sub(
        r"\b(?:\d[ -]?){13,16}\b",
        "[REDACTED CARD]",
        text,
    )
</code></pre>
<h2 id="heading-how-to-structure-prompts-with-clear-boundaries">How to Structure Prompts with Clear Boundaries</h2>
<p>XML-style tags are ordinary text, not special API commands. They make each part of a prompt explicit:</p>
<pre><code class="language-python">prompt = """
&lt;customer_reviews&gt;
The product is comfortable, but the available colours are limited.
Customers also describe it as durable.
&lt;/customer_reviews&gt;

&lt;sales_data&gt;
January: 120 units
February: 150 units
March: 98 units
&lt;/sales_data&gt;

&lt;task&gt;
Compare the reviews with the sales data.
Identify possible relationships and state uncertainty.
&lt;/task&gt;
"""
</code></pre>
<p>Here, <code>&lt;customer_reviews&gt;</code> identifies reference material, <code>&lt;sales_data&gt;</code> identifies the data, and <code>&lt;task&gt;</code> identifies the instruction. Use similar boundaries for policies, user-generated content, examples, and output requirements.</p>
<h2 id="heading-how-to-use-a-system-prompt">How to Use a System Prompt</h2>
<p>A system prompt defines ShopHelper’s general behaviour:</p>
<pre><code class="language-python">system_prompt = """
You are ShopHelper, a friendly customer-support assistant.

Keep answers concise and clear.
Do not invent prices, policies, or order details.
If information is missing, ask for it.
"""
</code></pre>
<p>Pass it separately from the conversation:</p>
<pre><code class="language-python">response = client.messages.create(
    model=MODEL,
    max_tokens=500,
    system=system_prompt,
    messages=[
        {"role": "user", "content": "Where is my order?"}
    ],
)
</code></pre>
<p>Because the customer didn't provide an order number, ShopHelper should ask for one instead of guessing.</p>
<h2 id="heading-how-to-add-tools">How to Add Tools</h2>
<p>Claude can't directly access your database. A tool gives it a structured way to request information from your application:</p>
<pre><code class="language-python">def get_order_status(order_id):
    orders = {
        "ORD-1001": "shipped",
        "ORD-1002": "processing",
    }

    return {
        "order_id": order_id,
        "status": orders.get(order_id, "not_found"),
    }
</code></pre>
<p>The function accepts an order ID, looks it up, and returns predictable data. In production, the dictionary would be replaced by a database query. Claude doesn't execute the function. Your application does.</p>
<p>Describe the function with a schema:</p>
<pre><code class="language-python">tools = [{
    "name": "get_order_status",
    "description": "Get the current status of a customer order.",
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "An order ID such as ORD-1001."
            }
        },
        "required": ["order_id"],
    },
}]
</code></pre>
<p>Claude may return a <code>tool_use</code> block instead of a final answer:</p>
<pre><code class="language-text">type="tool_use"
id="toolu_example"
name="get_order_status"
input={"order_id": "ORD-1001"}
</code></pre>
<p>The <code>name</code> identifies the function, <code>input</code> contains its arguments, and <code>id</code> is needed when returning the result. A <code>stop_reason</code> of <code>"tool_use"</code> means your application should handle the request before asking Claude to continue.</p>
<h2 id="heading-how-to-handle-a-tool-use-response">How to Handle a Tool-Use Response</h2>
<p>A tool-use response is a response containing the <code>tool_use</code> block described above.</p>
<p>Validate the tool name, arguments, and user permissions before execution:</p>
<pre><code class="language-python">import re

ORDER_ID_PATTERN = re.compile(r"^ORD-\d{4}$")

def validate_tool_request(name, tool_input, current_user):
    if name != "get_order_status":
        return False, "Unknown tool"

    order_id = tool_input.get("order_id")

    if not isinstance(order_id, str):
        return False, "order_id must be a string"

    if not ORDER_ID_PATTERN.fullmatch(order_id):
        return False, "Invalid order ID format"

    if order_id not in current_user["order_ids"]:
        return False, "The customer cannot access this order"

    return True, None
</code></pre>
<p>A complete loop can then validate and execute the request:</p>
<pre><code class="language-python">def run_conversation(user_text, current_user):
    messages = [{"role": "user", "content": user_text}]

    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=500,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason != "tool_use":
            return "".join(
                block.text
                for block in response.content
                if block.type == "text"
            )

        messages.append({
            "role": "assistant",
            "content": response.content,
        })

        results = []

        for block in response.content:
            if block.type != "tool_use":
                continue

            valid, error = validate_tool_request(
                block.name,
                block.input,
                current_user,
            )

            if valid:
                result = get_order_status(block.input["order_id"])
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result),
                })
            else:
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": error,
                    "is_error": True,
                })

        messages.append({
            "role": "user",
            "content": results,
        })
</code></pre>
<p>The <code>tool_use_id</code> connects the result to the original request. The application remains responsible for authorisation and execution.</p>
<h2 id="heading-claude-responses-can-contain-multiple-blocks">Claude Responses Can Contain Multiple Blocks</h2>
<p>This assumption is fragile:</p>
<pre><code class="language-python">answer = response.content[0].text
</code></pre>
<p>It assumes that the first block exists and is text. Instead, inspect each block:</p>
<pre><code class="language-python">for block in response.content:
    if block.type == "text":
        print(block.text)
    elif block.type == "tool_use":
        print("Validate and execute:", block.name)
    elif block.type == "thinking":
        continue
    else:
        print("Unhandled block type:", block.type)
</code></pre>
<p>ShopHelper displays text, validates and executes approved tool requests, doesn't display internal thinking, and logs unknown block types.</p>
<h2 id="heading-workflows-vs-agents">Workflows vs Agents</h2>
<p>A workflow follows a predefined sequence:</p>
<pre><code class="language-text">Receive ticket
↓
Extract details
↓
Draft reply
↓
Review reply
</code></pre>
<pre><code class="language-python">def ask(prompt, max_tokens=500):
    response = client.messages.create(
        model=MODEL,
        max_tokens=max_tokens,
        messages=[{"role": "user", "content": prompt}],
    )

    return "".join(
        block.text
        for block in response.content
        if block.type == "text"
    )


def handle_ticket_workflow(ticket):
    details = ask(
        f"&lt;ticket&gt;{ticket}&lt;/ticket&gt;\n"
        "&lt;task&gt;Extract the problem and desired outcome.&lt;/task&gt;"
    )

    draft = ask(
        f"&lt;details&gt;{details}&lt;/details&gt;\n"
        "&lt;task&gt;Draft a concise support reply.&lt;/task&gt;"
    )

    review = ask(
        f"&lt;draft&gt;{draft}&lt;/draft&gt;\n"
        "&lt;task&gt;List unsupported promises, or say OK.&lt;/task&gt;"
    )

    return draft, review
</code></pre>
<p>An agent is more flexible: Claude decides whether to use a tool and what to do next. Agents still require validation and a maximum step count. The <code>run_conversation()</code> function above can be reused inside an agent loop.</p>
<p>Use workflows when the steps are known and repeatability matters. Use agents when the next action depends on the current result.</p>
<h2 id="heading-chaining-parallelisation-routing-and-evaluator-optimizer">Chaining, Parallelisation, Routing, and Evaluator-Optimizer</h2>
<p><strong>Chaining</strong> passes each result to the next stage:</p>
<pre><code class="language-python">def chained_reply(ticket, policy):
    draft = ask(
        f"&lt;ticket&gt;{ticket}&lt;/ticket&gt;\n"
        "&lt;task&gt;Draft a support reply.&lt;/task&gt;"
    )

    issues = ask(
        f"&lt;policy&gt;{policy}&lt;/policy&gt;\n"
        f"&lt;draft&gt;{draft}&lt;/draft&gt;\n"
        "&lt;task&gt;List unsupported claims.&lt;/task&gt;"
    )

    return ask(
        f"&lt;draft&gt;{draft}&lt;/draft&gt;\n"
        f"&lt;issues&gt;{issues}&lt;/issues&gt;\n"
        "&lt;task&gt;Rewrite the final reply.&lt;/task&gt;"
    )
</code></pre>
<p><strong>Parallelisation</strong> runs independent tasks concurrently:</p>
<pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor

tickets = [
    "My headphones arrived broken.",
    "I was charged twice.",
    "How do I change my address?",
]

def summarise(ticket):
    return ask(
        f"&lt;ticket&gt;{ticket}&lt;/ticket&gt;\n"
        "&lt;task&gt;Summarise in one sentence.&lt;/task&gt;",
        max_tokens=100,
    )

with ThreadPoolExecutor(max_workers=3) as pool:
    summaries = list(pool.map(summarise, tickets))

digest = ask(
    "&lt;summaries&gt;\n"
    + "\n".join(summaries)
    + "\n&lt;/summaries&gt;\n"
    "&lt;task&gt;Summarise today's support themes.&lt;/task&gt;"
)
</code></pre>
<p><strong>Routing</strong> classifies a request before selecting a specialised workflow:</p>
<pre><code class="language-python">def route(ticket):
    label = ask(
        f"&lt;ticket&gt;{ticket}&lt;/ticket&gt;\n"
        "&lt;task&gt;Return exactly refund, delivery, or general.&lt;/task&gt;",
        max_tokens=10,
    ).strip().lower()

    return label if label in {"refund", "delivery", "general"} else "general"
</code></pre>
<p><strong>Evaluator-optimizer</strong> generates, reviews, and revises an answer:</p>
<pre><code class="language-python">def improve_reply(ticket, rounds=2):
    reply = ask(
        f"&lt;ticket&gt;{ticket}&lt;/ticket&gt;\n"
        "&lt;task&gt;Write a support reply.&lt;/task&gt;"
    )

    for _ in range(rounds):
        review = ask(
            f"&lt;reply&gt;{reply}&lt;/reply&gt;\n"
            "&lt;task&gt;List accuracy or tone problems, or say PASS.&lt;/task&gt;"
        )

        if review.strip().upper() == "PASS":
            break

        reply = ask(
            f"&lt;reply&gt;{reply}&lt;/reply&gt;\n"
            f"&lt;review&gt;{review}&lt;/review&gt;\n"
            "&lt;task&gt;Rewrite the reply.&lt;/task&gt;"
        )

    return reply
</code></pre>
<p>Use chaining for dependent stages, parallelisation for independent work, routing for specialised paths, and evaluator-optimizer loops when additional quality justifies extra API calls.</p>
<h2 id="heading-how-to-evaluate-prompt-quality">How to Evaluate Prompt Quality</h2>
<p>Use representative test cases:</p>
<pre><code class="language-python">test_cases = [
    {
        "ticket": "I want a refund for broken headphones.",
        "expected": "refund",
    },
    {
        "ticket": "Where is ORD-1002?",
        "expected": "delivery",
    },
    {
        "ticket": "Do you sell gift cards?",
        "expected": "general",
    },
]
</code></pre>
<p>These cases cover different request types. Run the same cases after changing the system prompt, examples, model, token limit, or routing instructions:</p>
<pre><code class="language-python">def evaluate(route_fn, cases):
    passed = 0

    for case in cases:
        result = route_fn(case["ticket"])

        if result == case["expected"]:
            passed += 1
        else:
            print("Failed:", case["ticket"], result)

    score = passed / len(cases)
    print(f"{passed}/{len(cases)} passed")
    return score
</code></pre>
<p>Use code-based graders for labels and JSON. Use human or model-based graders for tone, accuracy, and helpfulness.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building with the Claude API involves more than writing prompts. A reliable application needs structured context, managed conversation state, validated tool execution, deliberate response handling, suitable workflows, and repeatable evaluation.</p>
<p>The goal isn't to find one perfect prompt. It's to build a system around Claude that provides the right context, limits unsafe actions, handles uncertainty, and measures whether changes improve the result.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
