# deep_research A local-first, multi-agent deep-research CLI: give it a topic, it searches the web, cross-checks what it finds, and writes up a cited report β€” entirely on infrastructure you control, with no cloud LLM API key and no query ever leaving your machine. ``` $ deep_research "trends in AI customer-support chatbots" πŸ”Ž Researching... 🧐 Reviewing findings... ✍️ Writing report... # AI Customer-Support Chatbots: 2025–2026 Trends ... ``` ## Why this exists This started as a "does deep research actually work end-to-end" exercise and turned into a small case study in building an *agentic* system that survives contact with reality: models that hit their turn budget mid-task, search providers that rate-limit, and reviewers that reject good-faith work. The [case study](./docs/case-study.md) walks through what broke and how each failure was fixed, not just papered over. ## Architecture Four small agents, each with one job, coordinated by plain Rust control flow β€” not a framework's agent graph, not an LLM deciding when to stop: ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” approve/reject β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ researcher β”‚ ───────────────► β”‚ reviewer β”‚ β”‚ (tool-using)β”‚ ◄─────────────── β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ gaps/feedback β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ turn budget exhausted β”‚ approved, β”‚ mid-investigation β”‚ or out of rounds β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ summarizer │──findings───►│ writer │──► report β”‚ (recovery) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` - **researcher** β€” a tool-calling agent (`search_web`, `fetch_page`) that gathers and cross-checks evidence, capped at a fixed model-call budget so a confused model can't loop forever. - **reviewer** β€” a separate, fresh-context agent that checks the researcher's conclusions actually follow from its cited sources, and either approves the findings or hands back concrete gaps for another pass. - **writer** β€” turns approved (or partial) findings into a structured, footnoted report, streamed to the terminal as it's generated. - **summarizer** (recovery path) β€” only runs when the researcher exhausts its turn budget before concluding on its own. It reconstructs a proper findings dump from the raw tool-call transcript rather than the run simply failing; see the case study for why this exists and how it degrades gracefully if the summarizer call itself fails. Everything runs against local models via [Ollama](https://ollama.com) and a self-hosted [SearXNG](https://searx.space) instance for search β€” no OpenAI/ Anthropic/Google API key, no third-party search API, nothing about the research topic leaves the host it runs on. That's a deliberate constraint, not a limitation: it's the same shape a privacy-sensitive customer deployment would need. ## Running it Prerequisites: - [Ollama](https://ollama.com) running locally with a tool-calling-capable model pulled (the researcher and reviewer/writer models are configured in [`src/models.rs`](./src/models.rs)) - A local [SearXNG](https://docs.searxng.org/) instance with its JSON API enabled (defaults to `http://localhost:8080`, overridable via `SEARXNG_URL`) ``` cargo run -- "your research topic" # or, with tracing spans on stderr instead of the progress spinner: cargo run -- -l info "your research topic" # give the researcher your own documents to draw on, alongside the web β€” # repeatable, and a directory contributes every file directly inside it # (one level deep, not recursive): cargo run -- --doc ./notes.txt --doc ./research-docs/ "your research topic" ``` Uploaded documents are embedded with a dedicated embedding model (see `EMBEDDING_MODEL` in [`src/models.rs`](./src/models.rs)) into an in-memory vector index, then the excerpts most relevant to the topic are retrieved and folded into the researcher's task alongside anything it finds on the web β€” the same footnote-citation scheme applies to both. ## Project layout Split one concern per file rather than one large module: | File | Responsibility | |---|---| | `main.rs` | Argument parsing, logging setup, and the single top-level call β€” no orchestration logic | | `research.rs` | The research/review round loop | | `researcher.rs` | The tool-calling research phase | | `review.rs` | The reviewer agent | | `summarizer.rs` | Max-turns recovery: reconstructs findings via a model call | | `writer.rs` | Turns findings into the final streamed report | | `history.rs` | Pure, unit-tested helpers for parsing a rig chat history into usable text | | `documents.rs` | Resolves `--doc` paths into embeddable documents | | `retrieval.rs` | Embeds documents into an in-memory vector index and retrieves relevant excerpts | | `tools.rs` | `search_web` (SearXNG) and `fetch_page` tool implementations | | `stream.rs` | Drains a streaming prompt response to the terminal | | `progress.rs` | The terminal spinner and per-phase emoji | | `models.rs`, `cli.rs`, `observability.rs` | Small shared config: model names, CLI args, tracing setup | ## Testing ``` cargo test # unit tests β€” pure functions, no network cargo test -- --ignored # + a live smoke test against SearXNG cargo clippy --all-targets ``` CI (`.forgejo/workflows/ci.yml`) runs formatting, lint, build, and the unit test suite on every push and PR.