Architecture
Why Dinoer is built the way it is: what it guarantees, what it refuses, and where its limits are — narrowed once already on real evidence.
How a research campaign runs
manifest.json
│ campagne.py
▼
SearXNG ── queries
▼
light HTTP fetch ──► real browser (only if the page does not suffice)
▼
collecte.jsonl + operations.jsonl
▼
OpenCode ── ranking, then the report
▼
rapport_<timestamp>.md
- The manifest is a JSON file of targets: a
queryfor SearXNG, a fixedurl, aproduit(find the one page that really is the product page), or atable_reference(a list of trusted domains for a subject). Only the campaign identifier and the targets are required. - Discovery. A
querytarget goes to the SearXNG instance you configured. That instance queries other search engines in turn, which Dinoer does not control. - Collection. Each page is fetched with a simple HTTP request, after its
robots.txthas been read. Only a page that does not suffice is escalated to a real browser, one Playwright process per call. A target that fails does not stop the others. - The corpus. One
collecte.jsonlper campaign, one line per successful extraction, andoperations.jsonl, which records every attempt, failed or not. - The report. The pages are ranked (by local embeddings when you give a
subject), cut to a size budget, and handed to OpenCode, which writes the
body of
rapport_<timestamp>.md. The list of sources is appended by Dinoer itself; the model never writes it.
shot.py and rpa.py are different: independent calls, one process each,
that finish as soon as they have answered. Why →
What leaves your machine, and by which door
| What | Where it goes | Who decides | In the code |
|---|---|---|---|
| Discovery queries | the SearXNG instance you configured, which queries other engines in turn | you, with DINOER_SEARXNG_URL | lib/searxng.py |
| Page requests | each site directly, its robots.txt read first | the targets in your manifest | lib/fetch_leger.py, rpa.py |
| Collected text | OpenCode’s model, hosted by default, for the report, for targeted extraction, and to choose a product page (the first 1,500 characters of each candidate) | you, with DINOER_OPENCODE_MODEL | lib/modeles.py, lib/synthese.py, lib/extraction.py, lib/selection_candidats.py |
| A subject, for a reference table | OpenCode’s model, the subject only, when no table exists for it yet | the table_reference target | lib/tables_reference.py |
| Embeddings | your local Ollama, http://localhost:11434: the text stays on the machine unless you point DINOER_OLLAMA_URL elsewhere | you | lib/vector.py |
| A notification | a ntfy server, https://ntfy.sh unless you set another: the campaign identifier and the number of sources, never the local path of the report. Only when a ntfy topic is set | you, with ntfy_topic, DINOER_NTFY_TOPIC, DINOER_NTFY_URL | campagne.py, lib/ntfy.py |
| Your credentials | nowhere: resolved inside the Playwright process, from the encrypted directory | — | lib/repertoire_chiffre.py |
Source: the code of version 1.0.1, read on 25 September 2026. The opencode/
models are hosted: that provider answers at opencode.ai/zen (per
opencode models --verbose, OpenCode 1.18.32). What the model is then allowed
to do with the web is on the trust page, with the
limit it does not cover.