Skip to main content
Language models are good at summarizing text and bad at remembering facts. This tutorial takes the facts out of the model’s memory and puts them in the prompt, so that every sentence in the output can be traced back to a page you fetched a moment earlier. We will build a command line tool that answers a question with a short, cited brief:
Along the way we will:
  1. Search the live web with /augment/search
  2. Decide which of those results are worth reading
  3. Convert the selected pages to Markdown with /augment/scrape
  4. Ask a chat model to write the brief, citing sources by number
  5. Wire the four stages into one script
Doing retrieval ourselves, rather than letting the model do it, is what makes the result auditable. We keep the exact list of pages that went into the prompt and can show a reader where each claim came from. If you would rather have Venice handle retrieval inside a single request, set venice_parameters.enable_web_search on a chat completion instead. The Web Search and Scraping guide compares the two approaches.

Setup

You need Python 3.9 or newer, the requests package, and a Venice API key. See Generating an API Key if you do not have one.
Create research.py and start with the imports and a shared header block that every call reuses:

1. Search the web

/augment/search takes a query and returns up to 20 ranked results. Brave is the default provider and applies Zero Data Retention. Google is also available and is proxied through Venice, so the query is never linked to you.
Each result is an object with four fields:
Two things about that response are worth knowing before you build on it. The content field arrives with HTML in it, because the provider wraps matched terms in <strong> tags. The HTML_TAG substitution above strips them so the snippet reaches the model as plain text. The date field is frequently an empty string. Many pages publish no machine readable date, so treat date as a hint you can use when present rather than a field you can sort or filter on.
limit must be between 1 and 20, and query must be between 1 and 400 characters. Values outside those ranges return HTTP 400 with a validation body. They are not clamped for you.

2. Choose which sources to read

Scraping all ten results would be slow, expensive, and largely redundant. Search engines return several pages from the same site, and documentation sites in particular return the same page in several languages, so the same content can appear three or four times under different URLs. Keeping only the highest ranked result per domain removes most of that duplication in a few lines:
Running it on the ten results above narrows them to four distinct sites:
This is the natural place to add your own judgment. You might allowlist domains you trust, drop results whose snippet never mentions the key terms, or prefer results that carry a recent date. Every filter you apply here is a decision the model no longer has a chance to get wrong.

3. Scrape the selected pages

/augment/scrape fetches a public URL and returns it as Markdown. It first asks the site for a native Markdown representation and falls back to browser based extraction when there is none. Some pages will fail, and a research tool should treat that as routine rather than fatal:
Both guards earn their place. The status check catches sites that refuse automated access, and the length check catches pages that return 200 but hand back a cookie banner or an empty shell instead of an article.
Scrape failures return a plain {"error": "..."} body with a readable message, for example X (formerly Twitter) blocks automated access to their content. X and Reddit are blocked outright. To include posts from X in an answer, use venice_parameters.enable_x_search on a chat completion instead.
The requests do not depend on each other, so run them in parallel. While we are here, cap how much of each page we keep:
The third page came back at exactly 12000 characters, which means it was longer than the budget and got truncated.
The char_budget is not a nicety. Search results regularly include aggregate pages such as sitemaps, changelogs, and llms-full.txt files, and a single one of those can return close to a million characters. Without a cap, one unlucky result decides what the whole request costs.

4. Write the brief

Now we hand the model the pages we collected, numbered, and ask for citations that refer to those numbers. The numbering in the prompt is what lets us turn a [2] in the output back into a URL later.
A low temperature keeps the wording close to the source text. Setting enable_web_search to off matches the default, but saying it out loud guarantees the model cannot quietly introduce a source that is missing from our reference list.

5. Put it together

The last piece runs the stages in order and appends the reference list that resolves the citation numbers:
Progress messages go to stderr, so you can redirect the brief on its own into a file:
Here is the top of the brief it produced, abbreviated:
Notice that source 3 is never cited in this excerpt. That is the behavior we want. The model used the sources that were relevant and left the rest alone, and because the citations are numbered you can see that at a glance.

Tuning the pipeline

Most of the wall clock time goes to the final chat completion, since four scraped pages add up to tens of thousands of tokens. These are the levers worth reaching for first:

Next steps

The pipeline you now have is a foundation rather than a finished product. A few directions worth exploring:
  • Cache scraped Markdown by URL so repeated questions do not refetch the same pages.
  • Store the Markdown as vectors with Embeddings and retrieve passages instead of whole pages.
  • Let the model plan several queries before searching, as the Private Research Agent demo does.
  • Read the brief out loud by piping it into Narrating Articles with Text-to-Speech.

Web Search and Scraping

Reference for the Search and Scrape endpoints.

Narrating Articles with Text-to-Speech

Turn the text you just generated into audio.

Embeddings

Index scraped Markdown instead of refetching it.

Private Research Agent

A larger agent that plans its own searches.