3.3 Deploy Agents with Anthropic’s Native Tool Use#

In chapter 3.2 we learned how to implement an agent from scratch using python libraries. In this chapter we’ll learn how implement an agent using Anthropic’s native tool use.

Compared to chapter 3.2, the imports here are minimal — since we’re using Anthropic’s native tool use, we don’t need ldp or any agent framework. The only library we need is the anthropic Python package, which provides a straightforward client for interacting with Claude. But cost and flexibility are downsides here.

We’ll use the same problem setup and tools as in chapter 3.2. The agent is built to analyze protein data and generate hypothesis for drug discovery.

Tools:

  • analyze_protein_sequence — computes basic biophysical properties

  • summarize_protein_role — asks the AI to summarize biological context

🚀 How to run the notebook

This tutorial can be launched using the rocket button at the top of the page.

Option 2 — MyBinder#

Launches a temporary cloud Jupyter environment directly in your browser.

⚠️ Binder environments can take a few minutes to build and start.

After the notebook loads, create a .env file in the notebook directory containing your API keys:

OPENAI_API_KEY=your_key_here
ANTHROPIC_API_KEY=your_key_here

Notes#

  • You only need API keys for the providers used in a given notebook.

  • Never commit or publicly share your API keys.

  • If a cell fails due to missing credentials, verify that your keys were loaded correctly before rerunning the cell.

If you’re using Google Colab, make sure to install the requirements using the cell below. You don’t have to do this if you’re using Binder.

!uv pip install anthropic biopython==1.86

Make sure you have set up your API key before running the following cells.

import os

LLM_API_KEYS = {
    "openai":    "OPENAI_API_KEY",
    "anthropic": "ANTHROPIC_API_KEY",
}

def get_api_key(llm: str = "anthropic") -> str:
    """
    Load API key for the specified LLM from Colab secrets,
    environment variable, or user input.
    
    Args:
        llm: LLM provider name. eg: 'openai', 'anthropic'
    
    Returns:
        API key string
    
    Example:
        api_key = get_api_key("anthropic")
    """

    llm = llm.lower()
    if llm not in LLM_API_KEYS:
        raise ValueError(
            f"Unknown LLM '{llm}'. Choose from: {list(LLM_API_KEYS.keys())}"
        )

    env_var = LLM_API_KEYS[llm]

    # 1. Try Colab secrets
    try:
        from google.colab import userdata
        key = userdata.get(env_var)
        if key:
            return key
    except ImportError:
        pass

    # 2. Try environment variable / .env file
    try:
        from dotenv import load_dotenv
        load_dotenv()
        key = os.environ.get(env_var)
        if key:
            return key
    except ImportError:
        pass

    raise ValueError(
        f"API key not found. Please set {env_var}:\n"
        f"  export {env_var}='your-key-here'\n"
        f"  or add it to a .env file"
    )

3.3.1 Set up the LLM client#

We instantiate the client using our API key, which will be used for all subsequent calls to the model. Think of the client as a messenger between your Python code and Anthropic’s servers. When you want Claude to do something, you don’t communicate with it directly over the internet yourself. Instead, you hand your request to the client object, which handles the networking, authentication, and formatting behind the scenes, and brings the response back to you. All subsequent interactions with Claude in this chapter will go through this client.

import anthropic

# Connect to the Claude AI model.
# Make sure you have set your API key as an environment variable.
client = anthropic.Anthropic(api_key=get_api_key(llm="anthropic"))

print("Libraries loaded and client ready.")

3.3.2 Define the tools#

As we discussed previously, tools are Python functions our agent can choose to call. Before deploying the agent, we need to tell Claude what tools it has access to and how to use them.

With Anthropic’s native tool use, tools are defined as JSON schemas and passed directly to the API and no external libraries are required. Each tool definition includes a name, a description that helps Claude decide when to use it, and an input_schema that specifies what parameters the tool expects.

Below we define our two tools:

  • analyze_protein_sequence: takes an amino acid sequence and returns biophysical properties like molecular weight and isoelectric point.

  • summarize_protein_role: takes a protein name and returns a plain-language summary of its biological function.

# ── TOOL 1: Analyze a protein sequence ──────────────────────────────────────
def analyze_protein_sequence(sequence: str) -> dict:
    """
    Takes a protein sequence (one-letter amino acid codes, e.g. 'MKTIIALSYIFCLVFA...')
    and returns basic biophysical properties using BioPython.
    """
    from Bio.SeqUtils.ProtParam import ProteinAnalysis 

    sequence = sequence.upper().strip()
    analysis = ProteinAnalysis(sequence)

    results = {
        "length":              len(sequence),
        "molecular_weight_Da": round(analysis.molecular_weight(), 2),
        "isoelectric_point":   round(analysis.isoelectric_point(), 2),
        "instability_index":   round(analysis.instability_index(), 2),
        "gravy_score":         round(analysis.gravy(), 3),   # hydrophobicity
        "amino_acid_percent":  {
            aa: round(pct, 1)
            for aa, pct in analysis.amino_acids_percent.items()
            if pct > 0   # only show amino acids actually present
        },
    }

    # Interpret some values for the non-expert
    results["is_stable"]     = results["instability_index"] < 40
    results["is_hydrophilic"] = results["gravy_score"] < 0

    return results


print("Libraries loaded and client ready.")
# ── TOOL 2: Summarize protein biological role ────────────────────────────────

def summarize_protein_role(protein_name: str, organism: str) -> dict:
    """
    Uses a separate Claude call to look up and summarize the biological
    role of a protein from its training knowledge.
    """
    import anthropic

    client = anthropic.Anthropic(api_key=get_api_key(llm="anthropic"))

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=400,
        messages=[{
            "role": "user",
            "content": (
                f"Provide a concise 3–4 sentence summary of the protein '{protein_name}' "
                f"in {organism}. Cover: (1) its biological function, "
                f"(2) which disease or condition it is associated with, "
                f"(3) why it is considered a drug target. Be factual and concise."
            )
        }]
    )
    return {"summary": response.content[0].text}

After writing the python functions for the two tools, we have to format the tool definition using JSON schemas and pass directly to the API.

tools = [
    {
        "name": "analyze_protein_sequence",
        "description": (
            "Analyzes a raw protein amino acid sequence and returns biophysical "
            "properties: length, molecular weight, isoelectric point, instability "
            "index, GRAVY hydrophobicity score, and amino acid composition."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "sequence": {
                    "type": "string",
                    "description": "The protein sequence in one-letter amino acid code, e.g. 'MKTII...'"
                }
            },
            "required": ["sequence"]
        }
    },
    {
        "name": "summarize_protein_role",
        "description": (
            "Summarizes the known biological role, disease association, and "
            "drug-target relevance of a named protein in a given organism."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "protein_name": {
                    "type": "string",
                    "description": "Common name or gene symbol of the protein, e.g. 'EGFR' or 'p53'"
                },
                "organism": {
                    "type": "string",
                    "description": "The organism, e.g. 'human', 'mouse', 'E. coli'"
                }
            },
            "required": ["protein_name", "organism"]
        }
    }
]

print("Tools defined.")

3.3.3 Build the agent loop#

At the heart of the agent is a back-and-forth conversation between your code and Claude. This loop continues until Claude has enough information to answer the question.

Unlike a standard API call where you send a prompt and get a response, tool use is inherently iterative: Claude may need to call analyze_protein_sequence, inspect the result, and then decide to also call summarize_protein_role before it can draw a conclusion. Each tool result gets added back into the conversation so Claude always has full context of what it has already looked up. The logic follows a straightforward cycle as shown below.

rag_framework
Figure 3.3.1: In an agent loop the LLM decides if it requires a tool, our code executes the tool, receives the result from the execution. This cycle will continue until the LLM decides to terminate the cycle.

📝 Note The LLM doesn’t run the tools itself. It simply tells you which tool it wants to call and what arguments to pass. Your code is responsible for actually executing the tool and returning the result.

def run_agent(user_question: str, verbose: bool = True) -> str:
    """
    Runs the drug discovery agent on a user question.
    
    Parameters
    ----------
    user_question : str
        The scientist's question (see examples below).
    verbose : bool
        If True, prints each reasoning step — great for learning!
    
    Returns
    -------
    str
        The agent's final answer.
    """
    import anthropic

    client = anthropic.Anthropic(api_key=get_api_key(llm="anthropic"))

    # A system prompt that sets the agent's personality and goals
    system_prompt = """You are a computational biology assistant specializing in drug discovery. When given a protein of interest, you:
    1. Use the analyze_protein_sequence tool to compute its biophysical properties.
    2. Use the summarize_protein_role tool to understand its biological context.
    3. Combine both results to generate 2–3 specific, testable drug discovery hypotheses.
    
    Always explain your reasoning in plain language suitable for a bench biologist."""

    messages = [{"role": "user", "content": user_question}]

    if verbose:
        print(f"🧪 Question: {user_question}\n")
        print("─" * 60)

    # ── Agent loop ────────────────────────────────────────────────────────────
    while True:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1500,
            system=system_prompt,
            tools=tools,
            messages=messages
        )

        # Did the model decide to call a tool?
        if response.stop_reason == "tool_use":

            # Collect all tool calls in this response
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name   = block.name
                    tool_inputs = block.input
                    tool_id     = block.id

                    if verbose:
                        print(f"🔧 Agent is calling tool: {tool_name}")
                        print(f"   Inputs: {tool_inputs}")

                    # ── Dispatch: call the right Python function ──────────────
                    if tool_name == "analyze_protein_sequence":
                        result = analyze_protein_sequence(**tool_inputs)
                    elif tool_name == "summarize_protein_role":
                        result = summarize_protein_role(**tool_inputs)
                    else:
                        result = {"error": f"Unknown tool: {tool_name}"}

                    if verbose:
                        print(f"   ✅ Result: {result}\n")

                    tool_results.append({
                        "type":        "tool_result",
                        "tool_use_id": tool_id,
                        "content":     str(result)
                    })

            # Send ALL tool results back to the model in one message
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user",  "content": tool_results})

        # The model is done — it returned its final answer
        elif response.stop_reason == "end_turn":
            final_answer = response.content[0].text

            if verbose:
                print("─" * 60)
                print("🤖 Agent's Final Answer:\n")
                print(final_answer)

            return final_answer

        # Safety valve: stop if something unexpected happens
        else:
            print(f"⚠️  Unexpected stop reason: {response.stop_reason}")
            break

3.3.4 Test the agent#

Now tet’s try our agent on a classic drug target: EGFR (Epidermal Growth Factor Receptor), a key protein in many cancers.

We’ll provide a short fragment of its sequence so the notebook runs quickly. In a real workflow, you’d paste the full sequence from UniProt.

In this example let’s use a truncated fragment of human EGFR (from UniProt P00533). You can find the full sequence from https://www.uniprot.org/uniprot/P00533

# Run the agent

egfr_sequence_fragment = (
    "MRPSGTAGAALLALLAALCPASRALEEKKVCQGTSNKLTQLGTFEDHFLSLQRMFNNCEVVLGNLEITYVQRNYDLSFLKTIQEVAGYVLIALNTVERIPLENLQIIRGNMYYENSYALAVLSNYDANKTGLKELPMRNLQEILHGAVRFSNNPALCNVESIQWRDIVSSDFLSNMSMDFQNHLGSCQKCDPSCPNGSCWGAGEENCQKLTKIICAQQCSGRCRGKSPSDCCHNQCAAGCTGPRESDCLVCRKFRDEATCKDTCPPLMLYNPTTYQMDVNPEGKYSFGATCVKKCPRNYVVTDHGSCVRACGADSYEMEEDGVRKCKKCEGPCRKVCNGIGIGEFKDSLSINATNIKHFKNCTSISGDLHILPVAFRGDSFTHTPPLDPQELDILKTVKEITGFLLIQAWPENRTDLHAFENLEIIRGRTKQHGQFSLAVVSLNITSLGLRSLKEISDGDVIISGNKNLCYANTINWKKLFGTSGQKTKIISNRGENSCKATGQVCHALCSPEGCWGPEPRDCVSCRNVSRGRECVDKCNLLEGEPREFVENSECIQCHPECLPQAMNITCTGRGPDNCIQCAHYIDGPHCVKTCPAGVMGENNTLVWKYADAGHVCHLCHPNCTYGCTGPGLEGCPTNGPKIPSIATGMVGALLLLLVVALGIGLFMRRRHIVRKRTLRRLLQERELVEPLTPSGEAPNQALLRILKETEFKKIKVLGSGAFGTVYKGLWIPEGEKVKIPVAIKELREATSPKANKEILDEAYVMASVDNPHVCRLLGICLTSTVQLITQLMPFGCLLDYVREHKDNIGSQYLLNWCVQIAKGMNYLEDRRLVHRDLAARNVLVKTPQHVKITDFGLAKLLGAEEKEYHAEGGKVPIKWMALESILHRIYTHQSDVWSYGVTVWELMTFGSKPYDGIPASEISSILEKGERLPQPPICTIDVYMIMVKCWMIDADSRPKFRELIIEFSKMARDPQRYLVIQGDERMHLPSPTDSNFYRALMDEEDMDDVVDADEYLIPQQGFFSSPSTSRTPLLSSLSATSNNSTVACIDRNGLQSCPIKEDSFLQRYSSDPTGALTEDSIDDTFLPVPEYINQSVPKRPAGSVQNPVYHNQPLNPAPSRDPHYQDPHSTAVGNPEYLNTVQPTCVNSTFDSPAHWAQKGSHQISLDNPDYQQDFFPKEAKPNGIFKGSTAENAEYLRVAPQSSEFIGA"
)

question = f"""
I'm studying the role of EGFR in non-small-cell lung cancer.
Here is a fragment of its amino acid sequence:

{egfr_sequence_fragment}

Please analyze this protein and help me think about potential small-molecule drug targeting strategies.
"""

answer = run_agent(question, verbose=True)

3.3.5 Recap of agent set up#

Step

Agent Actions

1️

Received your biology question

2️

Decided to call analyze_protein_sequence → got MW, pI, stability…

3️

Decided to call summarize_protein_role → got biological context

4️

Combined both results → generated drug hypotheses

Test this agent with a protein of your choice by re-running the agent with a new protein sequence. You can also add more tools such as literature searching or plotting tools to analyze your data. For example that you can use the RAG pipeline we learned in section 2.2 as a tool here to extract literature data.