Step 1: Search and read
Kellmere's visitor desk wants an assistant that answers questions from the web and shows where each fact came
from. The lab pod has no internet, so webfixture.py plays the web: a search engine and a few dozen pages about
the town. Like the real web, it has official pages, outdated blog posts, a spam site, a forum, a page that never
answers and a missing page. Everything goes through it as an HTTP proxy (harness.PROXIES).
An agent is only as good as its tools. A page arrives as HTML full of scripts, menus and cookie banners, and every character of that costs tokens and distracts the model. A tool that raises on a 404 kills the whole run.
1. Write web_search(query, n=5) in research.py: call the search API and return the first n results.
2. Write fetch_page(url, max_chars=2500): return the page's readable text, title and date. For a page that
cannot be read, return {"url": ..., "error": ...} and do not raise.
3. Run and compare the three fetches: a real page, a missing one and one that never answers.
research.py, the file you edit95 lines
"""A research agent for Kellmere's visitor desk: it searches the (sandbox) web, reads pages and answers with
citations."""
import json
import re
import requests
from bs4 import BeautifulSoup
import harness
# ---------- Step 1: tools ----------
def web_search(query, n=5):
"""Top n results from the search engine: a list of {"title", "url", "snippet"}."""
# TODO (Step 1): GET harness.SEARCH_URL with params={"q": query}, proxies=harness.PROXIES and timeout=5;
# return the first n items of the JSON's "results" list.
raise NotImplementedError("Step 1: write web_search()")
def fetch_page(url, max_chars=2500):
"""The readable text of a page: {"url", "title", "date", "text"}; or {"url", "error"} when the page cannot be
read (an HTTP error, a timeout after 5 seconds, a connection problem). Drop scripts, styles, navigation, footers
and the cookie banner; keep the <main> text with whitespace collapsed, cut to max_chars. "date" comes from
<meta name="date"> (None if the page has none)."""
# TODO (Step 1): GET the url through harness.PROXIES with timeout=5. Timeouts, connection errors and non-200
# answers return {"url": url, "error": "..."} (never raise). Otherwise parse with BeautifulSoup(r.text, "html.parser"),
# decompose script/style/nav/footer tags and .cookie-banner, take the text of <main> (or <body>) with
# get_text(" "), collapse whitespace, cut to max_chars, and read the date from <meta name="date" content=...>.
raise NotImplementedError("Step 1: write fetch_page()")
TOOLS = [
{"type": "function", "function": {"name": "web_search", "description": "Search the web. Returns titles, URLs and snippets.",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}},
{"type": "function", "function": {"name": "fetch_page", "description": "Read a web page by URL. Snippets are not enough to answer from: read the page.",
"parameters": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]}}},
]
SYSTEM = """You are the research assistant for Kellmere's visitor desk. Today is 23 September 2026.
Answer questions about Kellmere by searching the web and reading pages.
Every page you read is numbered. Cite each fact with the number of the page it came from, like [2]."""
MIN_SOURCES = 2 # pages to read before an answer is trusted
# ---------- Step 2: the agent loop ----------
def page_for_model(n, page):
"""How a fetched page is shown to the model."""
return json.dumps(page, ensure_ascii=False)
def run_tool(name, args, sources):
"""Run one tool call and return the text for the model. Successfully fetched pages are appended to `sources`
(a list of page dicts) and numbered from 1 in that order."""
if name == "web_search":
results = web_search(str(args.get("query", "")))
return json.dumps(results, ensure_ascii=False)
if name == "fetch_page":
url = str(args.get("url", ""))
seen = [i for i, p in enumerate(sources, 1) if p["url"] == url]
if seen:
return f"You already read this page: it is [{seen[0]}]."
page = fetch_page(url)
if "error" in page:
return page_for_model(0, page)
sources.append(page)
return page_for_model(len(sources), page)
return f"Unknown tool {name!r}."
def run_agent(question, max_steps=8):
"""Search and read until the model answers. Returns {"answer", "sources" (fetched pages, numbered from 1),
"steps" (model calls made)}."""
raise NotImplementedError("run_agent() arrives in Step 2")
# ---------- Step 3: check the citations ----------
def check_citations(answer, sources):
"""{"cited": sorted page numbers the answer cites that exist, "bad": sorted numbers that match no fetched page,
"uncited": sentences that contain a digit but no [n] citation}. Citations look like [2] or [1, 3]."""
return {"cited": [], "bad": [], "uncited": []}
def citation_problem(answer, sources):
"""None when every fact in the answer cites a page that was actually read; otherwise the message that sends
the model back to work."""
return None
def with_references(result):
"""The answer followed by a numbered list of the pages it cites."""
refs = check_citations(result["answer"], result["sources"])["cited"]
lines = [f"[{n}] {result['sources'][n - 1]['title']} ({result['sources'][n - 1]['url']})" for n in refs]
return result["answer"] + ("\n\nSources:\n" + "\n".join(lines) if lines else "")harness.pyquestions.jsontry_it.pywebfixture.py