Add CI/CD, README, and a case study documenting this session's work
- .forgejo/workflows/deep_research-ci.yml: build, test, clippy (-D
warnings), and fmt --check on push/PR, scoped to deep_research (not
workspace-wide — swear_cleanup has an unrelated pre-existing clippy
warning that would otherwise break CI on an unrelated project)
- README.md: what the project does, the four-agent architecture, why
it's local-first (Ollama + self-hosted SearXNG, no cloud API key, no
query leaves the host), project layout, and how to run/test it
- docs/case-study.md: narrative walkthrough of the max-turns recovery
path, the DuckDuckGo-rate-limiting root cause and SearXNG fix, and the
separation-of-concerns refactor — each step verified against a live
run of the actual failing case, not just unit tests. Uses a neutral
"AI customer-support chatbot trends" research run as the illustrative
clean-pipeline example rather than the personal topic used during
actual debugging.
2026-08-18 11:28:29 +00:00
|
|
|
|
# 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`)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
Extract swear_cleanup to its own repo, flatten deep_research to root
deep_research is the only project this repo is meant to showcase, so the
Cargo workspace wrapping it and an unrelated side project no longer earns
its keep:
- swear_cleanup moved to a new standalone local repo (~/dev/swear_cleanup,
not pushed anywhere) via `git subtree split`, with its pre-workspace-
split history (when it lived at src/swear_cleanup/ in a single shared
crate) spliced onto its post-split history rather than starting from a
single flattened snapshot. FINDINGS.md, which was sitting at this repo's
root but was actually swear_cleanup's own build log, went with it.
- deep_research/{src,Cargo.toml,README.md,docs} moved to the repo root;
the [workspace] table collapsed into a plain [package] manifest with
dependency versions inlined from the old [workspace.dependencies].
- Cargo.toml keeps an explicit empty [workspace] table (not just omitted)
so that checking this repo out as a nested git worktree — this
project's own normal workflow — can't accidentally inherit a stale
ancestor directory's workspace manifest, which is exactly what broke
the build while testing this change from a worktree.
- .forgejo/workflows/deep_research-ci.yml -> ci.yml, dropping the now-
meaningless -p deep_research scoping and path filters (redundant when
it's the only thing in the repo).
- README.md and docs/case-study.md updated for the flattened commands
(cargo run/test with no -p flag); their relative links to each other
and to src/ were already correct since both moved together.
Verified: cargo build/test/clippy/fmt all clean from the new repo root.
2026-08-18 11:43:26 +00:00
|
|
|
|
cargo run -- "your research topic"
|
Add CI/CD, README, and a case study documenting this session's work
- .forgejo/workflows/deep_research-ci.yml: build, test, clippy (-D
warnings), and fmt --check on push/PR, scoped to deep_research (not
workspace-wide — swear_cleanup has an unrelated pre-existing clippy
warning that would otherwise break CI on an unrelated project)
- README.md: what the project does, the four-agent architecture, why
it's local-first (Ollama + self-hosted SearXNG, no cloud API key, no
query leaves the host), project layout, and how to run/test it
- docs/case-study.md: narrative walkthrough of the max-turns recovery
path, the DuckDuckGo-rate-limiting root cause and SearXNG fix, and the
separation-of-concerns refactor — each step verified against a live
run of the actual failing case, not just unit tests. Uses a neutral
"AI customer-support chatbot trends" research run as the illustrative
clean-pipeline example rather than the personal topic used during
actual debugging.
2026-08-18 11:28:29 +00:00
|
|
|
|
|
|
|
|
|
|
# or, with tracing spans on stderr instead of the progress spinner:
|
Extract swear_cleanup to its own repo, flatten deep_research to root
deep_research is the only project this repo is meant to showcase, so the
Cargo workspace wrapping it and an unrelated side project no longer earns
its keep:
- swear_cleanup moved to a new standalone local repo (~/dev/swear_cleanup,
not pushed anywhere) via `git subtree split`, with its pre-workspace-
split history (when it lived at src/swear_cleanup/ in a single shared
crate) spliced onto its post-split history rather than starting from a
single flattened snapshot. FINDINGS.md, which was sitting at this repo's
root but was actually swear_cleanup's own build log, went with it.
- deep_research/{src,Cargo.toml,README.md,docs} moved to the repo root;
the [workspace] table collapsed into a plain [package] manifest with
dependency versions inlined from the old [workspace.dependencies].
- Cargo.toml keeps an explicit empty [workspace] table (not just omitted)
so that checking this repo out as a nested git worktree — this
project's own normal workflow — can't accidentally inherit a stale
ancestor directory's workspace manifest, which is exactly what broke
the build while testing this change from a worktree.
- .forgejo/workflows/deep_research-ci.yml -> ci.yml, dropping the now-
meaningless -p deep_research scoping and path filters (redundant when
it's the only thing in the repo).
- README.md and docs/case-study.md updated for the flattened commands
(cargo run/test with no -p flag); their relative links to each other
and to src/ were already correct since both moved together.
Verified: cargo build/test/clippy/fmt all clean from the new repo root.
2026-08-18 11:43:26 +00:00
|
|
|
|
cargo run -- -l info "your research topic"
|
2026-08-18 12:27:54 +00:00
|
|
|
|
|
|
|
|
|
|
# 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"
|
Add CI/CD, README, and a case study documenting this session's work
- .forgejo/workflows/deep_research-ci.yml: build, test, clippy (-D
warnings), and fmt --check on push/PR, scoped to deep_research (not
workspace-wide — swear_cleanup has an unrelated pre-existing clippy
warning that would otherwise break CI on an unrelated project)
- README.md: what the project does, the four-agent architecture, why
it's local-first (Ollama + self-hosted SearXNG, no cloud API key, no
query leaves the host), project layout, and how to run/test it
- docs/case-study.md: narrative walkthrough of the max-turns recovery
path, the DuckDuckGo-rate-limiting root cause and SearXNG fix, and the
separation-of-concerns refactor — each step verified against a live
run of the actual failing case, not just unit tests. Uses a neutral
"AI customer-support chatbot trends" research run as the illustrative
clean-pipeline example rather than the personal topic used during
actual debugging.
2026-08-18 11:28:29 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-18 12:27:54 +00:00
|
|
|
|
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.
|
|
|
|
|
|
|
Add CI/CD, README, and a case study documenting this session's work
- .forgejo/workflows/deep_research-ci.yml: build, test, clippy (-D
warnings), and fmt --check on push/PR, scoped to deep_research (not
workspace-wide — swear_cleanup has an unrelated pre-existing clippy
warning that would otherwise break CI on an unrelated project)
- README.md: what the project does, the four-agent architecture, why
it's local-first (Ollama + self-hosted SearXNG, no cloud API key, no
query leaves the host), project layout, and how to run/test it
- docs/case-study.md: narrative walkthrough of the max-turns recovery
path, the DuckDuckGo-rate-limiting root cause and SearXNG fix, and the
separation-of-concerns refactor — each step verified against a live
run of the actual failing case, not just unit tests. Uses a neutral
"AI customer-support chatbot trends" research run as the illustrative
clean-pipeline example rather than the personal topic used during
actual debugging.
2026-08-18 11:28:29 +00:00
|
|
|
|
## 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 |
|
2026-08-18 12:27:54 +00:00
|
|
|
|
| `documents.rs` | Resolves `--doc` paths into embeddable documents |
|
|
|
|
|
|
| `retrieval.rs` | Embeds documents into an in-memory vector index and retrieves relevant excerpts |
|
Add CI/CD, README, and a case study documenting this session's work
- .forgejo/workflows/deep_research-ci.yml: build, test, clippy (-D
warnings), and fmt --check on push/PR, scoped to deep_research (not
workspace-wide — swear_cleanup has an unrelated pre-existing clippy
warning that would otherwise break CI on an unrelated project)
- README.md: what the project does, the four-agent architecture, why
it's local-first (Ollama + self-hosted SearXNG, no cloud API key, no
query leaves the host), project layout, and how to run/test it
- docs/case-study.md: narrative walkthrough of the max-turns recovery
path, the DuckDuckGo-rate-limiting root cause and SearXNG fix, and the
separation-of-concerns refactor — each step verified against a live
run of the actual failing case, not just unit tests. Uses a neutral
"AI customer-support chatbot trends" research run as the illustrative
clean-pipeline example rather than the personal topic used during
actual debugging.
2026-08-18 11:28:29 +00:00
|
|
|
|
| `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
|
|
|
|
|
|
|
|
|
|
|
|
```
|
Extract swear_cleanup to its own repo, flatten deep_research to root
deep_research is the only project this repo is meant to showcase, so the
Cargo workspace wrapping it and an unrelated side project no longer earns
its keep:
- swear_cleanup moved to a new standalone local repo (~/dev/swear_cleanup,
not pushed anywhere) via `git subtree split`, with its pre-workspace-
split history (when it lived at src/swear_cleanup/ in a single shared
crate) spliced onto its post-split history rather than starting from a
single flattened snapshot. FINDINGS.md, which was sitting at this repo's
root but was actually swear_cleanup's own build log, went with it.
- deep_research/{src,Cargo.toml,README.md,docs} moved to the repo root;
the [workspace] table collapsed into a plain [package] manifest with
dependency versions inlined from the old [workspace.dependencies].
- Cargo.toml keeps an explicit empty [workspace] table (not just omitted)
so that checking this repo out as a nested git worktree — this
project's own normal workflow — can't accidentally inherit a stale
ancestor directory's workspace manifest, which is exactly what broke
the build while testing this change from a worktree.
- .forgejo/workflows/deep_research-ci.yml -> ci.yml, dropping the now-
meaningless -p deep_research scoping and path filters (redundant when
it's the only thing in the repo).
- README.md and docs/case-study.md updated for the flattened commands
(cargo run/test with no -p flag); their relative links to each other
and to src/ were already correct since both moved together.
Verified: cargo build/test/clippy/fmt all clean from the new repo root.
2026-08-18 11:43:26 +00:00
|
|
|
|
cargo test # unit tests — pure functions, no network
|
|
|
|
|
|
cargo test -- --ignored # + a live smoke test against SearXNG
|
|
|
|
|
|
cargo clippy --all-targets
|
Add CI/CD, README, and a case study documenting this session's work
- .forgejo/workflows/deep_research-ci.yml: build, test, clippy (-D
warnings), and fmt --check on push/PR, scoped to deep_research (not
workspace-wide — swear_cleanup has an unrelated pre-existing clippy
warning that would otherwise break CI on an unrelated project)
- README.md: what the project does, the four-agent architecture, why
it's local-first (Ollama + self-hosted SearXNG, no cloud API key, no
query leaves the host), project layout, and how to run/test it
- docs/case-study.md: narrative walkthrough of the max-turns recovery
path, the DuckDuckGo-rate-limiting root cause and SearXNG fix, and the
separation-of-concerns refactor — each step verified against a live
run of the actual failing case, not just unit tests. Uses a neutral
"AI customer-support chatbot trends" research run as the illustrative
clean-pipeline example rather than the personal topic used during
actual debugging.
2026-08-18 11:28:29 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
Extract swear_cleanup to its own repo, flatten deep_research to root
deep_research is the only project this repo is meant to showcase, so the
Cargo workspace wrapping it and an unrelated side project no longer earns
its keep:
- swear_cleanup moved to a new standalone local repo (~/dev/swear_cleanup,
not pushed anywhere) via `git subtree split`, with its pre-workspace-
split history (when it lived at src/swear_cleanup/ in a single shared
crate) spliced onto its post-split history rather than starting from a
single flattened snapshot. FINDINGS.md, which was sitting at this repo's
root but was actually swear_cleanup's own build log, went with it.
- deep_research/{src,Cargo.toml,README.md,docs} moved to the repo root;
the [workspace] table collapsed into a plain [package] manifest with
dependency versions inlined from the old [workspace.dependencies].
- Cargo.toml keeps an explicit empty [workspace] table (not just omitted)
so that checking this repo out as a nested git worktree — this
project's own normal workflow — can't accidentally inherit a stale
ancestor directory's workspace manifest, which is exactly what broke
the build while testing this change from a worktree.
- .forgejo/workflows/deep_research-ci.yml -> ci.yml, dropping the now-
meaningless -p deep_research scoping and path filters (redundant when
it's the only thing in the repo).
- README.md and docs/case-study.md updated for the flattened commands
(cargo run/test with no -p flag); their relative links to each other
and to src/ were already correct since both moved together.
Verified: cargo build/test/clippy/fmt all clean from the new repo root.
2026-08-18 11:43:26 +00:00
|
|
|
|
CI (`.forgejo/workflows/ci.yml`) runs formatting, lint, build, and the unit
|
|
|
|
|
|
test suite on every push and PR.
|