JJoeven

Projects/ReAct Research Agent/Part 2

Fake Web Search and Open

Build a closed corpus, a token-overlap search tool, and an open(url) reader that truncates pages and never invents URLs.

The environment for a research agent is a corpus with a search API. Yours fits in a dict. That is how you get reproducibility. This part implements search and open with honest behavior: search never returns a URL that open cannot load; open never fetches a URL that is not in WEB.

Corpus design

Give each page a title, body, and optional tags. Keep bodies short (2–4 sentences). Long pages waste the Try it box and the context window. Include:

  • A canonical fact page (Acme founding)
  • A later update page (CEO change)
  • A distractor that shares tokens (Acme in an unrelated ad)
  • A empty-ish page that ranks on keywords but has no fact

Distractors teach ranking. If search is if query_word in body, ads will pollute. A tiny scoring function is worth it.

Search: token overlap

Lowercase, split on non-letters, ignore words shorter than 3 characters. Score = number of overlapping tokens with title+body. Return top k=3 with snippets: first 80 characters of body. Snippets are what the model sees without opening. That is realistic. It is also how models jump to conclusions — they finish from a snippet. Your evals will catch that if the snippet is incomplete.

Open: the only way to cite

open(url) returns {url, title, body} or {"error": "not_found"}. Truncate body to 500 characters. Log every successful open in a session_opened set the loop will use for citation checks. The tool itself can return the page; the loop owns the set.

Never fuzzy-match URLs. http://wiki.example/acme is not https://wiki.example/acme. Models drop letters. not_found is the correct answer; the policy may search again.

Live Pythonpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Step-by-step environment rules

  1. Closed world. If it is not in WEB, it does not exist. This is how you test cannot-answer.
  2. Search returns URLs only from WEB.keys(). Never synthesize a Wikipedia link because the question feels encyclopedic.
  3. Snippets are incomplete on purpose. The Ada interview's CEO name might be after character 80 — then the model must open the page. Tune a page so the fact is beyond the snippet. That is a teaching trap you want.
  4. open is idempotent. Opening twice returns the same body. No counters unless you are testing rate limits.

Make one fact live past the snippet

Edit the CEO sentence so it starts after 80 characters, or set snippet to 40 in your copy. Then a lazy policy that finishes after search will fail the CEO question. That is an eval in part 4.

Do not implement PageRank

Token overlap is enough. If you want a slightly better ranking, add a bonus if all query tokens appear in the title. Stop there. You are not shipping a search engine. You are shipping an agent environment.

Note:Prompt injection in web pages is a part-5 topic. For now, bodies are boring prose. Boring is a gift.

How the model should see search results

The policy prompt should list tools and a short observation, not the entire WEB dict. If you dump every page into the system prompt, you have not built retrieval — you have built a tiny context window with extra steps. Search exists to hide pages. Open exists to reveal one. That discipline is what you will reuse in RAG (next project) and in production browsers (URL allowlists).

Print hit URLs and snippets only. If you find yourself copying body into the search observation, undo it. The CEO-after-character-80 trap only works if search is lossy.

Exercise

Add a page https://wiki.example/kenji that says Ito became CEO in 2021. Confirm search("Kenji Ito CEO") returns it. Confirm open of a typo URL errors. Print hit URLs only — the model should see a short list, not the full web dict.

Check your understanding

Why must search only return URLs that exist in WEB?