{"slug": "build-your-first-ai-agent-from-scratch-in-python-a-web-research-agent", "title": "Build your first AI agent from scratch in Python: a web research agent", "summary": "Humanbound published a single-file Python web research agent that runs on any OpenAI-compatible endpoint with one dependency, httpx, and four tools: search Wikipedia, fetch a page, save a note and read notes. In a recorded run on 23 October 2026, Ollama's llama3.1:8b answered from search snippets without fetching a page and got the question wrong, which the post attributes to small local models being weak at tool calling. The post states the agent is not hardened: fetched text can steer the model, persisted notes can carry a hostile page's influence into later runs, and fetch_url's private-address check is vulnerable to DNS rebinding because the name is resolved once for the check and again by the HTTP client.", "body_md": "A web research agent in one Python file, with no agent framework. It has four tools (search Wikipedia, fetch a page, save a note, read notes), a loop, stop rules, and a SQLite notes file that survives between runs. It accompanies the post \"Build an AI agent from scratch in Python\" on the Humanbound blog.\n\nAny OpenAI-compatible endpoint works. `uv` installs the one dependency (`httpx`) from the file's header.\n\n```\nexport AGENT_API_KEY=...                         # your key\nexport AGENT_BASE_URL=https://api.openai.com/v1  # default\nexport AGENT_MODEL=gpt-4o-mini                   # default\nuv run agent.py \"Who coined the term 'prompt injection', and how is it different from jailbreaking?\"\n```\n\nThe trace goes to stderr, the answer to stdout. `--dump messages.json` writes the full message history.\n\nOpenRouter: set `AGENT_BASE_URL=https://openrouter.ai/api/v1` and a model id such as `openai/gpt-5.6-luna`.\n\nLocal with Ollama, no key: `AGENT_BASE_URL=http://localhost:11434/v1 AGENT_MODEL=llama3.1:8b uv run agent.py \"...\"`.\nSmall local models are weak at tool calling. In the recorded run (`examples/trace-ollama-llama3.1-8b.txt`)\nthe model answered from search snippets without fetching a page and got the answer wrong.\n\nSet `PRICE_IN_PER_MTOK` and `PRICE_OUT_PER_MTOK` to see a cost estimate in the trace. It is computed from\nthe API's `usage` numbers, not read from a bill.\n\nThe tests need no network and no key. The model is a scripted local server.\n\n```\nuv run --with httpx --with pytest pytest tests\n```\n\nThe traces, answers and message histories quoted in the post, recorded on 23 October 2026, plus\n`first-attempt/`, the runs from before `read_notes` was fixed to match on any word. `page-sizes.txt` comes\nfrom `measure_pages.py`; token counts there are characters divided by four.\n\nThe recorded runs predate a review pass that hardened the HTML extractor, the notes queries and the error\nhandling in `run_tool`, and reworded the `read_notes` tool description. The logic the traces show is unchanged.\n`postfix/` holds two runs of the hardened code on the same question. The second one reads the notes the first one saved.\n\nThis agent is not hardened, and it is meant to be read before it is trusted.\n\n- Fetched text is written by strangers. The model can be steered by it.\n- Notes persist, so a note written because of a hostile page can mislead a later run.\n- `fetch_url` will request any public address, including one that carries data out in its query string.\nIt refuses private and loopback addresses, and it does not follow redirects, so each hop is checked.\nThe check resolves the name once and the HTTP client resolves it again when it connects, so a hostile\nDNS server can pass the check and then point at a private address (DNS rebinding). A production agent\nshould connect to the address it checked.\n- It does not read `robots.txt` .\n- Cost is bounded only by `MAX_STEPS` and`TOKEN_BUDGET` .\n\n[Humanbound](https://github.com/humanbound/humanbound) is an open-source tool that tests an agent like this\nover HTTP. It finds what it thinks to try, so a clean result is not proof of safety.\n\nApache-2.0.", "url": "https://wpnews.pro/news/build-your-first-ai-agent-from-scratch-in-python-a-web-research-agent", "canonical_source": "https://github.com/iayanpahwa/research-agent-from-scratch", "published_at": "2026-10-26 11:13:18+00:00", "updated_at": "2026-10-26 11:50:04.499232+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-safety"], "entities": ["Humanbound", "Python", "httpx", "OpenAI", "Ollama", "llama3.1:8b", "OpenRouter", "uv"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/build-your-first-ai-agent-from-scratch-in-python-a-web-research-agent", "markdown": "https://wpnews.pro/news/build-your-first-ai-agent-from-scratch-in-python-a-web-research-agent.md", "text": "https://wpnews.pro/news/build-your-first-ai-agent-from-scratch-in-python-a-web-research-agent.txt", "jsonld": "https://wpnews.pro/news/build-your-first-ai-agent-from-scratch-in-python-a-web-research-agent.jsonld"}}