112 lines
5 KiB
Markdown
112 lines
5 KiB
Markdown
|
|
# 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 -p deep_research -- "your research topic"
|
|||
|
|
|
|||
|
|
# or, with tracing spans on stderr instead of the progress spinner:
|
|||
|
|
cargo run -p deep_research -- -l info "your research topic"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 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 |
|
|||
|
|
| `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 -p deep_research # unit tests — pure functions, no network
|
|||
|
|
cargo test -p deep_research -- --ignored # + a live smoke test against SearXNG
|
|||
|
|
cargo clippy -p deep_research --all-targets
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
CI (`.forgejo/workflows/deep_research-ci.yml`) runs formatting, lint, build,
|
|||
|
|
and the unit test suite on every push and PR.
|