Level 6 · Build Your Own Jarvis

Giving Jarvis Tools 🛠️

So far Jarvis can only talk. Tools let it act: look things up, do arithmetic reliably, control your smart home, run your Python. This is where an assistant becomes genuinely useful, and where you must be careful.

The idea: the model asks, your code acts

A model cannot check the weather or read your calendar; it can only produce text. Tool use bridges that gap. You describe some functions to the model. When it decides one would help, it does not run it (it cannot), it asks you to, with the arguments filled in. Your code runs the real function and hands the result back. The model then uses that result to answer.

You:    What's 4,891 times 7,237?
Model:  (I should use the calculator tool)  ->  multiply(4891, 7237)
Your code: runs multiply(4891, 7237) = 35,396,167
Model:  4,891 times 7,237 is 35,396,167.

Notice why this matters even for arithmetic: the model is a text predictor and is genuinely bad at multiplying large numbers (Lesson 53). Give it a calculator tool and it becomes perfectly accurate, because the actual maths happens in your reliable Python, not in the model's head.

Defining a tool

A tool is a name, a description, and a schema for its inputs. The description is how the model decides when to use it, so write it well: this is prompt engineering, not paperwork.

calculator_tool = {
    "name": "calculate",
    "description": "Evaluate a basic arithmetic expression and return the exact result. "
                   "Use this whenever the user asks for a calculation, since you are "
                   "unreliable at arithmetic on your own.",
    "input_schema": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "A simple arithmetic expression, e.g. '4891 * 7237'",
            }
        },
        "required": ["expression"],
    },
}

print(calculator_tool["name"])
print("described in", len(calculator_tool["description"]), "characters")
calculate
described in 170 characters

The input_schema is JSON Schema, the same shape you met when typing dictionaries in Lesson 38. It tells the model exactly what arguments to provide, and the API guarantees they will match.

The tool-use loop

Here is the full pattern. It is a loop: call the model, and if it asks for a tool, run the tool, feed the result back, and call again, until it stops asking and gives a final answer.

import anthropic

client = anthropic.Anthropic()


def calculate(expression):
    """Safely evaluate a simple arithmetic expression. No arbitrary code (Lesson 52)."""
    import ast
    import operator

    ops = {ast.Add: operator.add, ast.Sub: operator.sub,
           ast.Mult: operator.mul, ast.Div: operator.truediv,
           ast.Pow: operator.pow, ast.USub: operator.neg}

    def ev(node):
        if isinstance(node, ast.Constant):
            return node.value
        if isinstance(node, ast.BinOp):
            return ops[type(node.op)](ev(node.left), ev(node.right))
        if isinstance(node, ast.UnaryOp):
            return ops[type(node.op)](ev(node.operand))
        raise ValueError("unsupported expression")

    return ev(ast.parse(expression, mode="eval").body)


TOOLS = [{
    "name": "calculate",
    "description": "Evaluate an arithmetic expression exactly.",
    "input_schema": {
        "type": "object",
        "properties": {"expression": {"type": "string"}},
        "required": ["expression"],
    },
}]


def ask_with_tools(question):
    messages = [{"role": "user", "content": question}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )

        # If the model is done (not asking for a tool), we have our answer
        if response.stop_reason != "tool_use":
            return response.content[0].text

        # Append the model's turn (which contains the tool request)
        messages.append({"role": "assistant", "content": response.content})

        # Run every tool the model asked for, collect the results
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                if block.name == "calculate":
                    result = calculate(block.input["expression"])
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result),
                    })

        # Send the results back as a user turn, then loop
        messages.append({"role": "user", "content": tool_results})


if __name__ == "__main__":
    print(ask_with_tools("What is 4891 times 7237, and is it more than 35 million?"))

The loop, in plain English:

  1. Ask the model, giving it the tool definitions.
  2. If it did not ask for a tool (stop_reason is not "tool_use"), it has answered. Return that.
  3. Otherwise, run every tool it requested, matching each result to its tool_use_id.
  4. Send the results back and go round again. The model reads the results and either asks for another tool or answers.
🤖 The SDK can drive this loop for you

Writing the loop by hand, as above, shows you exactly what is happening, and that is the point of this lesson. In real projects the Anthropic SDK offers a tool runner that runs this loop automatically: you write the tool functions, it handles the call-run-feed-back cycle. Learn the manual version first so the automatic one is never magic.

The danger, stated plainly

PARANOIA[Legendary: Success]

Stop and understand what you have just built. The model decides which of your functions to run and with what arguments. If one of your tools deletes files, sends money, or runs shell commands, the model can now trigger that.

It is a text predictor. A cleverly worded message from a user, or text hidden in a web page your tool fetched, can steer it into calling a dangerous tool. This is called prompt injection, and it is unsolved. Design your tools as if a stranger on the internet is choosing when to call them, because in effect one is.

Rule for tool designWhy
Read-only tools are safe; acting tools are notFetching weather cannot hurt you; sending an email or deleting a file can
Never expose eval, exec, or a raw shellThe model could be steered into running anything (Lesson 52)
Validate every argument before actingThe model's arguments are as untrusted as user input (Lesson 22)
Confirm irreversible actions with the human'About to email your boss. Send? [y/N]' before it actually sends
Scope credentials to the minimumIf a tool needs an API key, give it one that can do only that one thing
Log every tool callSo you can see, after the fact, what your assistant actually did

Tools worth giving Jarvis

ToolDoesRisk
CalculatorReliable arithmeticNone. Give it freely
Current date/timeAnswers 'what day is it', which the model cannot knowNone
Web search / fetchFresh information past the training cutoffLow, but injected text in results can steer it
Read a fileAnswer questions about your documentsConfine it to one folder (Lesson 41)
Weather / stocks APILive dataLow; just an API call
Send an email / messageActually acts in the worldHigh. Confirm every send
Run shell / PythonMaximum powerExtreme. Sandbox or avoid

Some providers also offer server-side tools they run for you, such as web search and code execution in a sandbox, so you get the capability without building and securing the tool yourself. Convenient, and worth knowing they exist, but the safety thinking above still applies to anything you build.

Exercise 1

Give Jarvis the time and date

Write a get_datetime tool so Jarvis can answer 'what day is it' and 'how many days until Christmas'. This is the safest useful tool there is: read-only, no arguments, no risk.

Reveal solution
from datetime import date, datetime
import anthropic

client = anthropic.Anthropic()


def get_datetime():
    """Return today's date and time. The model cannot know this on its own."""
    now = datetime.now()
    days_to_christmas = (date(now.year, 12, 25) - now.date()).days
    return {
        "today": now.strftime("%A, %d %B %Y"),
        "time": now.strftime("%H:%M"),
        "days_until_christmas": days_to_christmas,
    }


TOOLS = [{
    "name": "get_datetime",
    "description": "Get the current date, time, and days until Christmas. "
                   "Use this for any question about what day or time it is.",
    "input_schema": {"type": "object", "properties": {}},
}]

# the tool-use loop is the same shape as the calculator example:
# call -> if stop_reason == "tool_use", run get_datetime(), feed the JSON back -> loop
print("Tool defined:", TOOLS[0]["name"])
print("Demo output:", get_datetime()["today"] is not None)

Note the tool returns a dictionary, which you would json.dumps into the tool_result content. The model reads structured data far more reliably than prose.

Exercise 2

Spot the dangerous tool

A tutorial online offers this tool to make an assistant 'really powerful'. Explain, specifically, the disaster waiting to happen.

run_command_tool = {
    "name": "run_command",
    "description": "Run any shell command and return its output.",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}

def run_command(command):
    import subprocess
    return subprocess.run(command, shell=True, capture_output=True, text=True).stdout
Reveal solution

This hands the model an unrestricted shell on your machine, and shell=True means the whole string is interpreted by the shell (Lesson 52). A user who types 'clean up my temp files' could, through a cleverly worded or injected prompt, cause the model to emit rm -rf ~. The model is a text predictor; it can be steered into emitting any command, and this tool will run it.

There is no safe way to offer 'run any command'. If you genuinely need code execution, run it in a locked-down sandbox (a container with no network, no important files, strict resource limits), or use a provider's server-side sandboxed execution and never touch your own machine. 'Powerful' and 'safe' are in tension here, and safe wins.

+100 XP