How it works¶
Architecture¶
Telegram chat (single user) Mini App console (menu button)
│ │ aiohttp on 127.0.0.1,
▼ ▼ cloudflared quick tunnel
python-telegram-bot (async) ──► run driver (bot/runs.py) ◄── webapp/api.py
one live drive per run · cancel by drain
· event bus → progress card / console
│
▼
intake → brief → scout ══╣ PLAN GATE ╠══ research → outline → compose → analyst → panel
panel → research (a coverage gap: a targeted, bounded gap wave, then outline in extend mode)
panel → compose (a targeted, bounded rewrite of the weak sections)
panel → report_builder ══╣ REPORT GATE ╠══ deliver → end
| Extend → extend_prep → research
| Revise → revise_prep → compose
quick path (/ask): intake → quick_answer → deliver
The two gates are LangGraph interrupts: after scout (plan approval over the
real sources live search found) and before deliver (report preview). The
run driver resumes them; it treats a gate as one only while no drive holds
the run.
What each stage does¶
| Stage | Job |
|---|---|
intake |
classify quick vs deep, refine the request |
brief |
sub-questions, success criteria, archetype, time sensitivity, entities — no URLs, so hallucinated links are structurally impossible; the links you typed are read first (GitHub metadata, README or page text, and at a repo or site root up to three parts that fit the question: docs, releases, changelog, pricing…), excerpted into the brief and pinned as sources |
scout |
one live discovery wave per sub-question across the search providers; decides the recency window |
| PLAN GATE | Approve / Edit / Cancel over the real found sources, with length, depth, engines-profile and recency knobs |
research |
waves of rerank → fetch (an SSRF-guarded native fetcher, Exa text, optional intel-stack rungs) → digest (an LLM reads each document into claims with verbatim-checked quotes) under one spend ledger; gap waves and Extend passes are targeted and budgeted |
outline |
sections with allocated evidence notes, shaped by the archetype; an Extend keeps the titles |
compose |
parallel section writers over their own notes (plain markdown), then finding extraction per section |
analyst |
takeaways with confidence labels, disagreements and scenarios, from the findings; for a question that asks for numbers, worked calculations — the model proposes formulas, the code checks each input against the finding it cites (or takes it as a labelled assumption) and evaluates them |
panel |
batched grounding judge (every finding judged against its cited quotes), coverage review, deterministic precision scan; routes to a gap wave, a targeted rewrite or the report |
report_builder |
cuts rejected claims, compiles the TL;DR from kept findings, banners and degradation notes, Sources, PDF, metadata.json, evidence.json |
| REPORT GATE | Send / Extend / Revise / Cancel |
deliver |
ends the run, or routes to Extend / Revise |
Design documents, on GitHub: the v4 spec, the v4 notes (what each work package built and why) and the v3 engine that v4 builds on.
Engines¶
engines/roles.py— the pipeline roles and their failure-policy classes.engines/schema.py/argus_engines.json— providers (base URL, key env var name, capabilities), profiles (tier engines + per-role bindings with fallbacks andon_exhausted=raise/skip_item/degrade), the transcriber block, dated prices.engines/registry.py— merges the defaults file, the optionalargus_engines.local.json(gitignored) and the library's/engineschoices; validates the schema at startup (no network) and the active profile at run start; resolves it into the snapshot every run carries.llm.py— the one facade,complete(role, …): retries, fallbacks, the declared policy, and aCallRecordper attempt (requested vs served model, routing) instate["model_calls"]andmetadata.json["engines"].- A declared fallback is never silent: it is a
degrade:/skip:line in the report's degradation notes and, past a threshold, a "⚠ Degraded run" banner.
Run configuration¶
Each run carries a RunConfig (graph/run_config.py): length, depth preset
(quick / standard / deep / exhaustive), recency, the research /
search / fetch / compose / panel knobs, and the engines snapshot. The
presets are the table in PRESETS; the console accepts dotted overrides.
Pre-v4 checkpoints resume with their length's legacy preset, recorded in the
run's errors and notes.
Project layout¶
argus/
├── README.md, handoff.md (front page; operations and migration notes)
├── mkdocs.yml (this site)
├── pyproject.toml
├── argus_engines.json (engine providers, profiles, transcriber, prices)
├── .env.example (NEVER commit .env)
├── docs/
│ ├── index.md, how-it-works.md, stylesheets/ (site pages)
│ ├── help.md (the /help full command guide)
│ ├── argus-v4-design.md (v4 build spec)
│ ├── v4-notes/ (one note per work package)
│ └── research-engine-v3.md (the v3 engine design)
├── src/argus/
│ ├── config.py (Settings, .env loader)
│ ├── llm.py (the LLM facade)
│ ├── engines/ (roles, schema, registry, driver, ledger,
│ │ URLs, fetchers, search, transcribers)
│ ├── graph/ (state, run_config, the nodes, assembly,
│ │ evidence export, precision, quarantine)
│ ├── bot/ (the Telegram package; run driver, progress)
│ ├── webapp/ (Mini App: server, auth, API, render, tunnel, static/)
│ ├── library.py (run + asset registry, aiosqlite)
│ ├── tools.py (intel-stack wrappers, search, PDF and YouTube helpers)
│ ├── media.py, transcribe.py (media downloads, transcription into the vault)
│ └── quarantine.py, citations.py, sources_block.py, cache_cleanup.py
├── bench/ (runner, deterministic metrics, compare, queries)
├── scripts/
│ ├── run.sh / run_mac.sh (pre-flight, then the bot)
│ ├── run_tests.sh, run_demo.sh, demo_run.py
│ ├── live_check.py, verify_live.sh (live end-to-end checks)
│ ├── engines_probe.py (probe a provider's request shapes)
│ ├── webapp_dev_url.py, sync_arsenal.py (console dev URL, kit snapshot)
│ └── reconcile_vault.py
├── tests/ (hermetic suite + live-proxy tests)
├── tools/bin/ (gitignored: cloudflared)
├── argus_checkpoints.sqlite (LangGraph checkpoints, one thread per run)
└── argus_library.sqlite (run + asset registry)
Tests¶
./scripts/run_tests.sh -q # the whole suite; the live-proxy tests need the proxy
# hermetic only, as CI runs it: see the --ignore list in .github/workflows/ci.yml
tests/test_llm.py and tests/test_graph.py are the live-proxy tests
(the shipped engines config resolved against the live proxy; a
network-deterministic graph drive where the LLM reads a controlled source).
Shared test helpers live in tests/helpers.py.
Verification rule: green tests ≠ a working bot. For research changes,
drive the real graph end-to-end: scripts/verify_live.sh (proxy check, the
live tests, then scripts/live_check.py, which prints an acceptance table and
exits non-zero on any failed check). The proxy re-routes models, so also
check served_model in a run's metadata.json. python -m bench.metrics and
python -m bench.compare measure runs deterministically.
Demo¶
./scripts/run_demo.sh
Drives the full graph on the active engines profile. The search wave and
page fetches are stubbed to a small controlled corpus so the run is
network-deterministic, but the LLM does the real work — it digests the corpus
into evidence notes, composes and analyses a sectioned report, and the panel
judges it. Output lands in demo_output/: the report folder (report.md,
report.pdf, metadata.json, evidence.json) and demo_transcript.json.
The demo prints, for every LLM call, the role, the requested model and
the served one.
Operational notes¶
- See
handoff.mdin the repository for running the bot, the console and tunnel, migration, and troubleshooting. - Rate limits: the free proxy throttles per IP and per key, and a run makes
many per-source calls. Only one run researches at a time; depth presets and
the per-run Exa budget bound the spend, and
/enginesshows this month's Exa cost. - Cancel drains: the run finishes its current step, keeps its checkpoint,
and
/continueresumes it. - Source provenance: every fetched item keeps its
markdown_pathon disk;metadata.jsonandevidence.jsonlist the models, sources, quotes and verdicts. - Trust, not a guarantee: the judge only sees the notes' quotes; a claim it cannot locate stays in the body and is listed as such, and an unjudged finding marks the report "⚠ Not fact-checked".
Secret handling¶
- API keys (
FREELLMAPI_API_KEY, the paid providers' keys,EXA_API_KEY) andTELEGRAM_BOT_TOKENare read from the environment /.envonly.argus_engines.jsonholds env var names, never values. Never logged, never hardcoded, never committed (.envis gitignored). TELEGRAM_ALLOWED_USER_IDgates the bot (and the console's signedinitData) to a single Telegram user; unset, every user is refused.
Where outputs live¶
Demo runs are written to demo_output/ inside the project. Real Telegram
reports default to the vault research-history folder; override the
location with ARGUS_REPORTS_ROOT (and the vault root with
ARGUS_VAULT_ROOT) in .env. The run + asset registry and the LangGraph
checkpoints stay in the project dir (argus_library.sqlite /
argus_checkpoints.sqlite), never in a synced vault. The console's tunnel log
is logs/cloudflared.log.