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:
- Ask the model, giving it the tool definitions.
- If it did not ask for a tool (
stop_reasonis not"tool_use"), it has answered. Return that. - Otherwise, run every tool it requested, matching each result to its
tool_use_id. - Send the results back and go round again. The model reads the results and either asks for another tool or answers.
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
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 design | Why |
|---|---|
| Read-only tools are safe; acting tools are not | Fetching weather cannot hurt you; sending an email or deleting a file can |
Never expose eval, exec, or a raw shell | The model could be steered into running anything (Lesson 52) |
| Validate every argument before acting | The 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 minimum | If a tool needs an API key, give it one that can do only that one thing |
| Log every tool call | So you can see, after the fact, what your assistant actually did |
Tools worth giving Jarvis
| Tool | Does | Risk |
|---|---|---|
| Calculator | Reliable arithmetic | None. Give it freely |
| Current date/time | Answers 'what day is it', which the model cannot know | None |
| Web search / fetch | Fresh information past the training cutoff | Low, but injected text in results can steer it |
| Read a file | Answer questions about your documents | Confine it to one folder (Lesson 41) |
| Weather / stocks API | Live data | Low; just an API call |
| Send an email / message | Actually acts in the world | High. Confirm every send |
| Run shell / Python | Maximum power | Extreme. 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.
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.
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).stdoutReveal 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.