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_outputbuilds a query from the current research state. - Web retrieval –
web_researchcalls one of the search helpers (duckduckgo_search,searxng_search, etc.) and deduplicates the results. - Summarisation & reflection –
summarize_sourcesandreflect_on_summaryfeed the LLM (viaget_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:
web_research→from_runnable_config(5 callers) to loadConfiguration.web_research→get_config_value(utility for env look‑ups).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).- Results →
deduplicate_and_format_sources→format_sources(both in utils.py). - Summarisation path:
web_research→generate_search_query_with_structured_output→get_llm→ChatLMStudio(instantiates the LLM client). ChatLMStudio→_generateruns 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 inutils.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_KEYis hard‑coded in.github/workflows/claude.yml. Move secret‑using steps to aworkflow_runjob 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-slimis not pinned by digest; usepython@sha256:<digest>. - Medium – No dependency‑vulnerability scan in CI; add
dependency-review-actionorosv-scanner. - Medium – Outbound HTTP calls lack a timeout (see
utils.py). Supplytimeout=or a configuredrequests.Session. - Medium – Checkout step retains the GITHUB_TOKEN; set
persist-credentials: falseand 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.