Tool-Calling CLI Assistant

Build a CLI assistant from scratch — raw SDK only, no frameworks — that answers questions using three tools: calculator, get_current_time, and read_file. This is the atom every later lab is built from. You start from an empty directory; the skeleton below is the map.

Before you start
You need the environment from the Setup lesson: Python 3.11+ in a virtualenv, pip install anthropic (or openai), and your API key exported. Add pip install pytest for the test criterion. No other dependencies. Budget roughly an afternoon for the core criteria.

What you're building

A terminal REPL: the user types a question, your loop calls the model with three tool schemas, executes whatever the model requests, feeds results back, and prints the final answer — plus a running token/cost report. It must survive multi-step questions like "what's 3 more than the number in numbers.txt?" (read_file → calculator → answer).

LLMreason + decideToolsyour code runsstartmessages[]USER“summarize my notes on RAG”ASSISTANTtool_call search_notes(“RAG”)TOOL→ 5 snippets [n12, n41 …]ASSISTANTtool_call read_note(n41)TOOL→ note body (820 tokens)ASSISTANT“Your RAG notes cover 3…” · no toolmessages = [ user task ]
Your lab in one picture: loop until stop_reason is end_turn.
1/6

Suggested structure

skeleton (fill in the TODOs)
# tools.py — implementations + schemas
def calculator(expression: str) -> str:
    # SAFELY evaluate arithmetic. No eval() on raw input — and note that
    # ast.literal_eval can't do math (literals only): walk an ast.parse()
    # tree allowing only number/operator nodes, or write a tiny parser.
    ...

def get_current_time(timezone: str = "UTC") -> str: ...
def read_file(path: str) -> str:
    # constrain to the working directory; return a clear error string
    # (not an exception) when the file doesn't exist
    ...

# agent.py — the loop
def run_turn(messages, budget):
    while True:
        resp = call_with_retries(lambda: client.messages.create(
            model=MODEL, max_tokens=1024, tools=SCHEMAS, messages=messages))
        budget.add(resp.usage)                 # track every call
        if resp.stop_reason != "tool_use":
            return resp
        messages.append({"role": "assistant", "content": resp.content})
        messages.append({"role": "user", "content": execute_all(resp.content)})
Design decisions that matter: tool errors are returned as strings (with is_error: true) so the model can recover; the calculator must not eval() arbitrary input; the loop needs a max-iteration guard so a confused model can't spin forever.

Ship it to your portfolio

This lab is the first artifact in your portfolio, and hiring managers routinely look at a candidate's GitHub before the résumé — 2–3 deep, evaluated projects beat a pile of shallow demos. Package it so a reviewer gets the signal in one minute:

  • README with a 60-second demo: a GIF or asciinema recording of the multi-step question (read_filecalculator → answer) plus copy-paste run instructions. Reviewers rarely clone the repo — the demo IS the first impression.
  • Reported numbers: pass count from the test_agent.py suite you wrote, tokens + estimated cost for a typical session, and observed retry behavior under a simulated 429. Real measured numbers signal you ran and verified it, not just wrote it.
  • An honest "Limitations" section: e.g. what expressions the calculator can't parse, the single-directory file sandbox, no conversation persistence across runs. Stated limitations read as engineering judgment, not weakness — and their absence is what reviewers notice.
  • A trace or screenshot of it working: one full multi-tool transcript (assistant tool call → tool result → final answer, with the token/cost report) so a reviewer can verify the loop without running anything.
Acceptance criteria — all must pass
0/6 verified
Stretch goals
  • Stream the final answer token-by-token while still handling tool-use turns
  • Add prompt caching with a cache breakpoint after the system prompt + tools, and log cache-read savings
  • Add a --think flag that enables adaptive thinking (thinking={'type': 'adaptive'}) and prints the model's reasoning summary before the final answer
  • Practical test: re-implement the minimal one-tool loop from memory in under 30 minutes

Be honest — the gates only mean something if the criteria really pass.

Your work

Repo URL, demo link, notes to your future self — saved locally with your progress, and handy when you package the portfolio.