feat(ai): gist report with call flow and etherscan hyperlinks - #338
Conversation
The AI gist was just the raw LLM detail. It now publishes a structured report: metadata header, the Telegram summary, a deterministic call flow, and the analysis. The call flow is built in Python from the decoded calldata (target, signature, ABI param names, ETH value, nested bytes payloads) rather than asked of the LLM, so it can't be hallucinated or summarized away. Every address is rendered full-length as a block-explorer link, labeled with its contract name / token symbol. For the LLM's own prose, the prompt now carries an Address Links section with the exact markdown per address and the system prompt requires every address in the detail to use it — the model copies a line instead of assembling an explorer URL (or picking the wrong chain's explorer). The TLDR stays plain text since it goes to Telegram unchanged. Gist titles go from a constant "AI Transaction Analysis" to "<contract> - <DD/MM/YYYY HH:MM> - <RISK>" so a list of reports is scannable. Two fixes found while validating live: - the DETAIL marker regex matched inside "## Detailed Analysis" and sliced the report down to "ed Analysis" - a leading "Detailed Analysis" heading now gets stripped so it doesn't double up with the report's own "## Analysis" looks_like_calldata / try_decode_inner_calldata / MAX_BYTES_RECURSION_DEPTH move to utils/calldata/decoder.py (their natural home) so report.py can share them without a circular import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hourly maple job was disabled in jobs.yaml (71c02bb) but monitoring.yaml still listed protocols/maple/main.py as a scheduled task, so test_monitoring_tasks_exist_in_jobs_yaml has been failing on main. Follows the euler/pendle/silo convention: the protocol entry is marked disabled while keeping its shared Safe/timelock monitors listed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tuple parameter such as configure((address,uint256)) fell through the scalar branch, so the report stringified the whole tuple — the address came out as raw lowercase text instead of a checksummed explorer link. The same gap in _extract_address_args meant those addresses never reached the label lookup or the prompt's Address Links section, leaving the LLM without a link to copy for them either. Arrays and tuples are now decomposed recursively (array_element_type / tuple_component_types) on both paths; iter_address_values walks the same type structure for collection. Plain address[] rendering is unchanged. decoder._split_top_level becomes public split_top_level_types so the report can decompose a tuple type without re-implementing the parser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Confirmed and fixed in 0df1c72. Reproduced first — The second half was the more damaging one: Now:
8 new tests in One related gap left deliberately out of scope: |
What
The AI explanation gist was just the raw LLM detail. It now publishes a structured report — metadata header, the Telegram summary, a deterministic call flow, and the analysis.
Example output
Both are the same Infinifi
addFarms(uint256,address[])timelock operation, generated by the real pipeline (live Etherscan labels + LLM).Changes
utils/llm/report.py— builds the gist page. The call flow comes straight from the decoded calldata (per-call target, signature, ABI parameter names, ETH value, nestedbytespayloads unwrapped) rather than being asked of the LLM, so it can't be hallucinated, re-ordered, or summarized away. Every address is rendered full-length (per the repo's "no truncated addresses" rule) as a link to the chain's explorer, annotated with its contract label / token symbol._build_promptnow emits an--- Address Links ---section containing the exact markdown for each address in the transaction, and the system prompt requires the detail to use it. The model copies a line instead of assembling an explorer URL or guessing which explorer a chain uses. The TLDR stays plain text, since it goes to Telegram unchanged.ReportContext.label_address, defaulting to the executing timelock/Safe. Safe multisend batches label the utility contract instead, so_explain_safe_tx()passes the outer target in that path.AI Transaction Analysisto<contract> - <DD/MM/YYYY HH:MM> - <RISK>(UTC), e.g.Infinifi Shorttimelock - 11/08/2026 10:04 - LOW, so a list of reports is scannable. Falls back to protocol name, then the old constant.Bugs found while validating live
_marker_patternmatched "Detail" inside a## Detailed Analysisheading, slicing the published report down toed Analysis— visible at the top of the "before" gist above. Fixed with a word-boundary lookahead.## Analysis.Refactor
looks_like_calldata/try_decode_inner_calldata/MAX_BYTES_RECURSION_DEPTHmoved fromai_explainer.pytoutils/calldata/decoder.py(their natural home) soreport.pycan share them without a circular import.collect_unique_addressesextracted so the label lookup and the links block cover exactly the same address set.Testing
tests/test_llm_report.py(25 cases) plus explainer-side coverage for the links section, address collection, title, and the marker-regex fix.uv run ruff format ./uv run ruff check .clean,uv run mypyclean for the touched files.uv run pytest tests/→ 752 passed, 0 failed. A second commit fixestest_monitoring_tasks_exist_in_jobs_yaml, which was already failing onmain: the hourly maple job was disabled injobs.yaml(71c02bb) butmonitoring.yamlstill listedprotocols/maple/main.pyas scheduled. Marked the entrydisabled: true, following the euler/pendle/silo convention (shared Safe/timelock monitors stay listed).🤖 Generated with Claude Code