Building the function before the AI ever sees it
This course's prerequisite (see the course README) is Lessons 13 and 14 of the langchain course. There, a calculator was built as a plain Python function first, tested directly with no model involved, and only afterward wrapped with @tool and handed to a model. This lesson follows that exact same shape, on purpose: get browse(url) right and predictable on its own first, so Lesson 16 can focus entirely on how a model decides to use it, not on whether the function itself works.
The code, piece by piece
def browse(url: str) -> str:The whole interface a future AI will ever see: one string in (a URL), one string out (the page's text). No Playwright-specific types cross this boundary, on purpose, a model doesn't know what a Page object is, and it doesn't need to.
try: page.goto(url, timeout=15_000)except Exception as exc: browser.close() return f"Could not load {url}: {exc}"Normal Python code often lets exceptions propagate, so whoever called it can decide how to handle a failure. A tool function can't rely on that: once a model is calling this (Lesson 16), there's no try/ except on the model's side, it can only read whatever text comes back. So failures have to become a normal return value, a string describing what went wrong, not something that crashes the whole program.
text = page.locator("body").inner_text()inner_text() (unlike inner_html(), used nowhere in this course until now) strips away every tag, script, and stylesheet, returning only the text a human eye would actually read on the rendered page. That's the right shape for a model: raw HTML is mostly markup a model doesn't need, and would burn through its limited context window for no benefit.
max_chars = 4000if len(text) > max_chars: text = text[:max_chars] + "...(truncated)"Real pages can be huge. A model's context window is a hard limit, not a suggestion, so trimming here keeps the tool's output small and predictable no matter what page it's pointed at. text[:max_chars] is Python slicing again (seen in Lesson 13): "the first max_chars characters."
Why no @tool decorator yet
@tool (from langchain_core.tools, used starting next lesson) adds metadata for a model: a name, a description, an argument schema. None of that is useful until a model actually exists to read it. Keeping this lesson AI-free means you can test and trust browse() entirely on its own terms, the same discipline good software engineering already asks for: build and verify a unit in isolation before composing it into something bigger.
Checkpoint
browse(url) -> str: a plain, LLM-agnostic function, string in, string out, no knowledge that a model will ever call it.- Failures become return values, not exceptions: a tool function a model will call can't rely on a
try/excepton the caller's side, it has to report failure as ordinary text. inner_text()vsinner_html():inner_text()gives you what a human sees rendered, with tags and scripts stripped, the right shape to hand to a model.- Truncating output: a model's context window is a hard limit; trimming long results keeps a tool's output predictable.
If anything here still feels unclear, ask before moving to Lesson 16, where this exact function gets wrapped with @tool and handed to a model.