Recipe: Search, Then Summarise¶
Goal¶
Search the web, read the best sources in full, and write a summary that cites them, with the
Web server's web_search, fetch_webpage and format_search_results.
Prerequisites¶
-
The
webextra, andextractfor main-content extraction (both are inall):pip install "doc-processing-std-tools[web,extract] @ git+https://github.com/rzoeb/doc-processing-std-tools.git@v0.6.1" -
A search provider account (Tavily, Serper or Brave) and its API key.
Steps¶
Tool calls are written as Python calls; the model sends the same arguments as JSON. The results are from a live run, shortened.
1. Configure the provider¶
DPTTOOLS_WEB_SEARCH_PROVIDER=tavily # or serper, brave
DPTTOOLS_WEB_SEARCH_API_KEY=tvly-xxx
Both go in the environment of the process that runs the tools: your own process in-process, or
the server's env out of process. fetch_webpage and format_search_results need no provider.
2. Search¶
web_search(query="LibreOffice headless convert-to pdf command line", num_results=5)
{
"provider": "tavily",
"query": "LibreOffice headless convert-to pdf command line",
"results": [
{"title": "Tip of the day: batch PDF conversion with LibreOffice - anarcat",
"url": "https://anarc.at/blog/2019-10-08-libreoffice-pdf-batch",
"snippet": "> Fear not, shell scripting and headless mode flies in to the rescue! ..."},
{"title": "Convert-to command line parameter",
"url": "https://ask.libreoffice.org/t/convert-to-command-line-parameter/840",
"snippet": "More importantly, headless conversion silently does nothing if any LO is already open; ..."}
],
"usage": {"provider_reported": true, "search_depth": "basic", "credits": 1,
"response_time": 1.1, "request_id": "..."}
}
num_resultsdefaults to 5 and is capped at 20.search_type="news"searches news, for recent events.- With Tavily,
search_depth="basic"costs 1 credit and"advanced"2.usagereports what Tavily charged. Serper and Brave report no cost, and std-tools doesn't invent one.
3. Read the best sources¶
A snippet is a fragment, not a source. Pick the two or three results that answer the question, and read each one:
fetch_webpage(url="https://anarc.at/blog/2019-10-08-libreoffice-pdf-batch", format="markdown")
{
"final_url": "https://anarc.at/blog/2019-10-08-libreoffice-pdf-batch/",
"status": 200,
"title": "Tip of the day: batch PDF conversion with LibreOffice - anarcat",
"extractor": "trafilatura",
"text": "# Tip of the day: batch PDF conversion with LibreOffice\n\nSomeone asked me today ...",
"total_chars": 1560,
"has_more": false,
"likely_js_rendered": false
}
- Long pages come in parts. When
has_moreis true, call again withoffset=next_offsetrather than raisingmax_length. likely_js_rendered: truemeans the text is probably incomplete. Retry withfetch_webpage_renderedif the browser server is attached (JavaScript-Rendered Pages).- A PDF or Office file is refused, with its size. Download it with
execute_commandand read it with the Documents tools. - Private and internal addresses are refused, and so is a redirect to one.
4. Number the sources and cite them¶
format_search_results turns results into numbered entries and a reference list. Pass it the pages
the model actually read, as a JSON array of {title, url, snippet}, so that the numbers match the
sources used. Here the snippets are the points the summary takes from each page:
format_search_results(results=json.dumps([
{"title": "Tip of the day: batch PDF conversion with LibreOffice - anarcat",
"url": "https://anarc.at/blog/2019-10-08-libreoffice-pdf-batch",
"snippet": "libreoffice --headless --convert-to pdf *.docx converts every DOCX file in the folder."},
{"title": "Convert-to command line parameter",
"url": "https://ask.libreoffice.org/t/convert-to-command-line-parameter/840",
"snippet": "Headless conversion silently does nothing if LibreOffice is already open."},
]))
[1] Tip of the day: batch PDF conversion with LibreOffice - anarcat
libreoffice --headless --convert-to pdf *.docx converts every DOCX file in the folder.
[2] Convert-to command line parameter
Headless conversion silently does nothing if LibreOffice is already open.
---
References:
[1] Tip of the day: batch PDF conversion with LibreOffice - anarcat — https://anarc.at/blog/2019-10-08-libreoffice-pdf-batch
[2] Convert-to command line parameter — https://ask.libreoffice.org/t/convert-to-command-line-parameter/840
Entries are numbered in the order given, with the titles given. The model writes its summary with
[1], [2] markers and ends with the reference list. The whole web_search output, passed as it
is, works too: the numbers then follow the search results, with their snippets.
5. Give an agent the tools¶
from doc_processing_toolkit import Agent
from doc_processing_std_tools import web_search_server
agent = Agent(
model="claude-sonnet-5",
instructions=(
"Answer with a short summary that cites its sources. Search with web_search, read the two "
"or three best results with fetch_webpage, and use only what you read. Number the sources "
"with format_search_results, cite them as [n] in the text, and end with the reference list."
),
tools=[{"type": "fastmcp", "server": web_search_server,
"allowed_tools": ["web_search", "fetch_webpage", "format_search_results"]}],
)
result = agent.run_sync("How do I convert a folder of DOCX files to PDF with LibreOffice?")
fetch_image is left out here because a summary doesn't need it. Add it when the model should look
at a chart or photo from a page: fetch_webpage(..., include_assets=True) lists a page's image
URLs.
What to validate¶
web_searchreturnsprovider,query,results(each withtitle,urlandsnippet) andusage, and Tavily'susageshows the credits charged.- Without the provider settings,
web_searchreturns anError:naming them, and the other three tools still work. fetch_webpagereturns the page's main text, and paging withoffsetreturns the rest.format_search_resultsnumbers the entries from 1, and its reference list matches them.- An unknown
search_typeorsearch_depthis an error, not a silent default. - Error messages never contain the API key or the provider's request URL.
Test sources¶
tests/unit/test_web_search.py: each provider (TestWebSearch), usage reporting (TestSearchUsage), citation formatting (TestFormatSearchResults), argument checks (TestWebSearchInputValidation) and the network policy (TestSSRFProtection). HTTP is mocked, so no key is needed.tests/unit/test_fetch_webpage_structured.py: thefetch_webpageresult, paging and the content-type check.tests/integration/test_web_search_mcp.py: the tools through the server.
Related pages¶
- Web server: every argument, the providers and the network policy.
- Configuration: the fetch limits (
DPTTOOLS_WEB_FETCH_*).