Skip to main content
Research agents are useful when you want more than a single search result or a quick model answer. A good research agent can turn a broad topic into search queries, collect sources, extract the important evidence, follow up on gaps, and write a cited briefing that you can inspect afterward. In this tutorial, we’ll build a private research agent using Python and the Venice API. By the end, you’ll have a CLI that can research a topic, scrape public pages into Markdown, summarize source chunks, run gap-aware follow-up research passes, and generate a cited report with optional local JSONL artifacts. Interested in the full code implementation? Check out the GitHub repo. Before we continue, you’ll need a Venice API key:

What We’re Building

The reference implementation is a small Python project with a few clear parts: The flow looks like this: Private research agent pipeline
  1. Ask Venice to generate diverse search queries for the topic.
  2. Search the web with one or more providers.
  3. Deduplicate URLs before reading them.
  4. Use Venice’s scrape endpoint to turn each public source page into Markdown.
  5. Split long pages into chunks.
  6. Ask Venice to extract evidence from each chunk.
  7. Ask Venice to turn chunk evidence into source notes.
  8. Identify research gaps and source-balance issues before generating follow-up queries.
  9. Ask Venice to synthesize the final report with footnote-style citations.
This is “private” in the practical sense that the agent keeps the orchestration, source notes, artifacts, and final reports on your machine. Venice handles the model calls and scraping through its API. The default reference implementation still sends search queries to DuckDuckGo or arXiv, so treat provider choice as part of your privacy design.

Setting Up the Project

The reference project uses Python 3.13 and uv, but the same code works with a normal virtual environment too. Create a new project:
Install the dependencies:
If you prefer pip, create a virtual environment and install the same packages:
Create a .env file for local development:
We use VENICE_MODEL so you can change the model without editing code. The reference implementation currently defaults to openai-gpt-55, but you can swap it for another chat model available to your Venice account.

Creating the Data Models

Before writing the agent logic, we’ll define the objects that move through the pipeline. These models keep the rest of the code easier to reason about because every source carries provenance: where it came from, which query found it, when it was retrieved, and how it was chunked. Create research_agent/models.py:
The important fields here are canonical_url, content_hash, and chunks. canonical_url lets the agent avoid reading the same source repeatedly when search results differ only by tracking parameters or fragments. content_hash helps catch duplicate pages even when they live at different URLs. chunks lets us summarize long pages in smaller pieces instead of losing useful evidence to context limits. Add the helper functions below the dataclasses:
Chunking is deliberately simple here: fixed-size character chunks with overlap. That is enough for a demo research agent because Venice’s scrape endpoint returns Markdown, which is usually much cleaner than raw HTML. For production research on long technical documents, you can improve this by splitting on headings, paragraphs, or token counts.

Building the Venice Client

Next, we’ll create a small Venice client. You could use the OpenAI Python SDK for chat completions because Venice is OpenAI-compatible, but the reference implementation uses httpx directly so the same client can call Venice’s POST /augment/scrape endpoint. Create research_agent/venice.py:
The from_env() helper keeps secrets out of your source code. It also makes local development convenient because python-dotenv can load VENICE_API_KEY and VENICE_MODEL from .env. Now add chat completions:
For the final report, we want to use streaming because deep reports can take significantly longer (because it will produce a lot more text). This can cause timeout issues for requests where it may take an extremely long time to produce the final output. By using streaming, we can eliminate this issue and make the request more resistant to timeout failures:
Then add scraping:
Venice’s scrape endpoint accepts a publicly accessible URL and returns the page as Markdown. That means the model does not need to parse raw HTML, and your source extraction prompts can work with cleaner text. The remaining helper handles retries and response parsing:
The complete repo also includes a robust _post_chat_stream() helper that reads server-sent events from streaming chat completions. You can start without streaming, then add it once the rest of the research flow works.

Adding Search Providers

The search layer has two jobs: find source URLs and fetch those URLs through the Venice scraper. The reference implementation uses DuckDuckGo’s HTML endpoint for general web search and arXiv’s Atom API for papers. Create research_agent/web.py:
Now add DuckDuckGo:
And arXiv:
The WebSearch class coordinates providers and fetches pages:
The full reference implementation adds retries, host-level request delays, and friendlier errors. Those are worth keeping because research agents spend a lot of time dealing with pages that block automation, redirect unexpectedly, or return transient errors. Add the small provider helpers at the bottom:

Writing Local Artifacts

For research workflows, auditability matters. If the final report says something surprising, you should be able to inspect which source led to it. Create research_agent/artifacts.py:
This writes one JSON object per line, which makes the artifacts easy to append, inspect, and process with command-line tools later.

Building the Research Agent

Now that we have Venice, search, models, and artifacts, we can build the actual agent. Create research_agent/agent.py:
The system prompt is the core behavioral guardrail. We don’t want the model to produce an impressive-sounding report from memory. We want it to use the source material and call out uncertainty when the evidence is thin. We also need two final dataclasses in models.py if you have not added them yet:
Next, define the ResearchAgent:
The run() method coordinates the research passes:
The two seen_* sets are what keep the agent from wasting time on duplicate sources. URL dedupe catches repeated links. Content hash dedupe catches mirrors, syndicated posts, and pages that redirect to the same final content.

Planning Initial and Follow-up Searches

The first model call turns the topic into search queries:
After each research pass, the updated agent does a more deliberate gap-analysis step. It looks at the current notes, counts source clusters by domain, asks Venice what coverage is missing, writes those gaps to artifacts, and then uses the resulting queries for the next pass. Gap analysis loop Start by tracking source balance:
This gives the agent a simple way to notice source-cluster capture. If every source is coming from one company, one framework, or one domain, follow-up queries should deliberately broaden the source set instead of collecting more of the same. Now use that balance information when creating follow-up searches:
The newer reference implementation wraps this in _gap_follow_up_queries(), which asks Venice to return both gap records and queries:
When --artifacts is enabled, these records are written to research_gaps.jsonl. That gives you a useful audit trail for why the agent searched for a particular second-pass query. The parser should be forgiving. If the model returns malformed JSON, the agent falls back to the original topic:
This pattern is worth using throughout agent code: ask for structured output, parse it, and provide a simple fallback when the output is not usable.

Reading and Summarizing Sources

Now we collect source notes. The agent searches each query, fetches each result through Venice scrape, chunks the Markdown, and summarizes the useful evidence.
Individual search and fetch failures should not stop the whole run. The public web is messy. Some pages block scraping, some return PDFs, some are down, and some redirect to unexpected places. A research agent should keep moving and record what failed. Here is the source-reading method:
For each source chunk, ask Venice for a short evidence summary and exact quotes:
Then collapse the chunk summaries into a source note:
This two-step summarization is the part that makes the agent feel more reliable than a basic “summarize these URLs” script. The model reads source chunks first, then writes a source-level note from those extracted pieces of evidence.

Writing the Final Report

Once the agent has source notes, it can write the report. Start with a single-pass report writer:
The reference implementation goes further for deep reports: it asks Venice for an outline, drafts each report section separately, then asks a final editor pass to assemble the finished report and convert internal source IDs into footnote-style citations. That staged approach is useful when you want long-form research output because one giant prompt often compresses too much. The updated prompts also push the report toward a broad, source-backed survey instead of a thin decision guide. If the source base is skewed toward one cluster, the editor prompt tells Venice to acknowledge that skew and avoid presenting it as representative of the whole field. Add the digest helpers:
Finally, add error recording:
At this point, the core research loop is in place.

Adding the CLI

Now we need a command-line entry point. Create main.py:
The CLI exposes the knobs you’ll actually tune during research: Now wire everything together:
This gives us a working local research CLI.

Running the Agent

Run a quick research pass:
Write the report to a Markdown file:
Use more sources and multiple providers:
Choose the final report style:
Use brief for a concise source-backed briefing, standard for a fuller survey, and deep for the staged outline/section/editor workflow. Save auditable artifacts:
When artifacts are enabled, you’ll see files like:
These files are useful when you want to understand how the agent reached a conclusion. For example, source_notes.jsonl shows the summarized source evidence, research_gaps.jsonl shows why follow-up searches were generated, and errors.jsonl shows pages that failed during search, scraping, or summarization.

Privacy and Reliability Notes

A research agent touches several systems, so it helps to be precise about what goes where: Private research agent data boundaries If you want to keep more of the search path inside Venice, you can adapt the provider layer to call Venice’s POST /augment/search endpoint instead of querying DuckDuckGo directly. The reference implementation uses lightweight public providers so the demo stays easy to run and understand. For reliability, keep these defaults conservative:
  • Use retries for Venice calls and web requests.
  • Add a small --request-delay if you are reading many pages from the same host.
  • Cap --max-sources so broad topics do not run indefinitely.
  • Save --artifacts for important reports so you can audit the final output.
  • Treat the report as a briefing, not ground truth. Follow citations back to the original source when accuracy matters.

Testing the Pieces

You do not need live web requests or Venice calls to test most of the system. The reference repo uses fake Venice and fake web classes to test the research loop, dedupe behavior, artifacts, and report prompts. A useful first test is URL canonicalization:
Then test that duplicate content gets skipped:
Fakes make agent tests much faster and less flaky. You can verify the orchestration logic without relying on live search results, network conditions, or model output.

Benchmarking

Many AI providers now have their own deep research workflows, so the reference repo includes a simple benchmark against Perplexity’s Deep Research tool. Both agents were asked to write a report on AI agent framework architecture, then the generated reports were checked into the GitHub repo. This is not meant to be a formal benchmark. It is a practical way to inspect report structure, source coverage, citation quality, and whether the agent over-focuses on one source cluster. That is also why the updated implementation tracks research_gaps.jsonl and source balance before follow-up searches.

Extending This Example

Once the baseline agent works, here are practical ways to improve it:
  • Add a Venice search provider using POST /augment/search.
  • Store reports and artifacts in a small SQLite database instead of JSONL files.
  • Add source allowlists or blocklists for trusted research domains.
  • Add PDF support by combining Venice scrape with document parsing for sources that do not expose clean HTML.
  • Add an evaluation set of topics and expected source types so you can compare research quality after prompt changes.
  • Add a review step that asks Venice to find unsupported claims in the final report before saving it.
The biggest upgrade is usually better source selection. Query generation helps, but you can also improve quality by preferring primary sources, standards documents, official docs, papers, changelogs, and dataset pages over low-signal summaries.

Finishing Up

Thanks for reading! Hopefully this helped you build a practical private research agent with Python and the Venice API. The useful pattern here is not just “ask a model to research something.” It is breaking research into auditable steps: plan searches, collect sources, extract evidence, write source notes, follow up on gaps, and synthesize with citations. By keeping those steps explicit, we get a research workflow that is easier to inspect, test, and improve over time.