The Problem

Users who want a fully local research assistant must stitch together a language model, web‑search APIs, and custom summarisation logic. Doing that manually leads to version drift, secret leakage, and brittle scripts that are hard to maintain.

What This Does

local-deep-researcher provides a single Python package that coordinates an LLM (via Ollama or LMStudio) with a configurable web‑search backend (DuckDuckGo, SearXNG, Tavily, Perplexity). The core workflow lives in src/ollama_deep_researcher/graph.py and implements:

  • Search‑query generation – generate_search_query_with_structured_output builds a query from the current research state.
  • Web retrieval – web_research calls one of the search helpers (duckduckgo_search, searxng_search, etc.) and deduplicates the results.
  • Summarisation & reflection – summarize_sources and reflect_on_summary feed the LLM (via get_llm → ChatLMStudio) to produce a markdown report and identify knowledge gaps.

Configuration (model URLs, API keys, number of cycles) is read from .env and wrapped by src/ollama_deep_researcher/configuration.py.

How It Is Wired

Execution starts with a call to web_research (the public entry point) in graph.py. The call chain is:

  1. web_research → from_runnable_config (5 callers) to load Configuration.
  2. web_research → get_config_value (utility for env look‑ups).
  3. web_research → one of the search functions (duckduckgo_search, searxng_search, tavily_search, perplexity_search, searxng_search). Each makes an outbound HTTP request (no timeout – see health).
  4. Results → deduplicate_and_format_sources → format_sources (both in utils.py).
  5. Summarisation path: web_research → generate_search_query_with_structured_output → get_llm → ChatLMStudio (instantiates the LLM client).
  6. ChatLMStudio → _generate runs the model inference.

The graph module owns eight functions and two types, acting as the hub (≈22 internal call edges). utils.py supplies nine helper functions; it is the only file that performs network I/O outside the search helpers. No circular imports exist, and each module is isolated: configuration.py, state.py, prompts.py, lmstudio.py, and __init__.py have zero import edges.

The repository touches external resources in two ways:

  • Model inference – via the LLM client in lmstudio.py.
  • Web search – via HTTP calls in the search helpers inside graph.py (and the single outbound call in utils.py).

Because the import graph is flat, changes to a helper function (e.g., strip_thinking_tokens) have limited ripple effects, but the deep nesting in utils.py (13‑level indentation) makes its logic hard to follow and increases cognitive load for modifications.

How To Use It

# Clone the forked repo (use the exact URL as required)
git clone https://github.com/moses-y/local-deep-researcher.git
cd local-deep-researcher

# Install dependencies (uv is recommended because uv.lock is present)
uv sync          # creates a virtualenv and installs from pyproject.toml

# Copy example environment and edit as needed
cp .env.example .env
# Edit .env: set LLM_PROVIDER, model name, API keys for the chosen search service

# Build and run the container (optional)
docker build -t local-deep-researcher .
docker run --rm -e $(cat .env | xargs) local-deep-researcher

The package does not expose a CLI script; programmatic use is via:

from ollama_deep_researcher.graph import web_research

report = web_research(topic="quantum computing", cycles=3)
print(report)

Adjust topic and cycles to control depth. The Configuration class will read values from the environment.

Real‑World Use

A knowledge‑base microservice can import web_research to generate on‑demand research briefs. For example:

def get_brief(topic):
    return web_research(topic=topic, cycles=2)

This call runs entirely locally, avoiding external SaaS costs while still harvesting fresh web content.

Code Health & Issues

  • Critical – Secret ANTHROPIC_API_KEY is hard‑coded in .github/workflows/claude.yml. Move secret‑using steps to a workflow_run job or gate them behind a protected environment.
  • High – GitHub Actions are referenced by mutable tags (anthropics/claude-code-action@beta, docker/setup-qemu-action@v3, etc.). Pin each to a full commit SHA.
  • Medium – Base image python:3.11-slim is not pinned by digest; use python@sha256:<digest>.
  • Medium – No dependency‑vulnerability scan in CI; add dependency-review-action or osv-scanner.
  • Medium – Outbound HTTP calls lack a timeout (see utils.py). Supply timeout= or a configured requests.Session.
  • Medium – Checkout step retains the GITHUB_TOKEN; set persist-credentials: false and pass the token only where needed.
  • Medium – Dockerfile runs as root; add a non‑root user and switch with USER.
  • Low – Workflow jobs have no timeout-minutes; set a reasonable bound to avoid overlapping runs.

Additional observations: the repo contains no test suite, and the Python dependencies are not locked (no requirements.txt or poetry.lock). The presence of uv.lock suggests a lockfile exists but is not used by the CI pipeline.

The Bottom Line

local-deep-researcher stitches together local LLMs and web search into a single, easy‑to‑invoke Python function, which is valuable for privacy‑focused research pipelines. However, the codebase suffers from security‑critical workflow secrets, missing dependency pinning, and a lack of automated tests, making it risky for production without remediation. It is best suited for internal prototypes or teams prepared to harden the CI/CD and add test coverage.