feat: add natural WhatsApp cascade delivery - #54455
Closed
devatnull wants to merge 9 commits into
Closed
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a metadata-controlled delivery style for WhatsApp gateway sends, adding a conservative “human cascade” mode that splits substantive multi-paragraph prose into a small sequence of natural DM-like bubbles while keeping status/progress chrome and other sensitive/structured content in a single message.
Changes:
- Add WhatsApp “human cascade” splitting with configurable thresholds, jittered per-bubble delays, and tail merging/chunking.
- Ensure non-final streaming commentary and gateway status/progress sends force
delivery_style=singlevia send metadata. - Add regression tests covering cascade thresholds, structured tails, stream-consumer metadata behavior, and status delivery style.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/platforms/whatsapp/adapter.py | Implements WhatsApp delivery style parsing and human-cascade splitting/delay behavior. |
| gateway/stream_consumer.py | Forces delivery_style=single for non-final stream sends and uses that metadata for commentary sends. |
| gateway/run.py | Forces delivery_style=single for status/progress sends (including fallback status sends). |
| tests/gateway/test_whatsapp_formatting.py | Adds coverage for cascade heuristics, forced styles, structured tails, and guardrails (code blocks/approvals/groups). |
| tests/gateway/test_stream_consumer.py | Adds unit coverage for _metadata_for_send() and commentary send metadata. |
| tests/gateway/test_gateway_status_delivery_style.py | Adds regression tests to ensure status/progress sends force single delivery style. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+418
to
+434
| self._human_cascade_max_total_chars = self._coerce_int_extra( | ||
| "human_cascade_max_total_chars", 900, env_var="WHATSAPP_HUMAN_CASCADE_MAX_TOTAL_CHARS" | ||
| ) | ||
| self._human_cascade_min_total_chars = self._coerce_int_extra( | ||
| "human_cascade_min_total_chars", 420 | ||
| ) | ||
| self._human_cascade_min_lead_chars = self._coerce_int_extra( | ||
| "human_cascade_min_lead_chars", 40 | ||
| ) | ||
| self._human_cascade_max_bubble_chars = self._coerce_int_extra( | ||
| "human_cascade_max_bubble_chars", 320, env_var="WHATSAPP_HUMAN_CASCADE_MAX_BUBBLE_CHARS" | ||
| ) | ||
| self._human_cascade_max_merged_bubble_chars = self._coerce_int_extra( | ||
| "human_cascade_max_merged_bubble_chars", | ||
| 640, | ||
| env_var="WHATSAPP_HUMAN_CASCADE_MAX_MERGED_BUBBLE_CHARS", | ||
| ) |
Comment on lines
+503
to
+506
| @staticmethod | ||
| def _has_approval_gate(text: str) -> bool: | ||
| """Return True for explicit approval/control prompts that must stay intact.""" | ||
| return bool(re.search(r"`/(approve|deny|reject|confirm|cancel|stop|new|reset)\b", text, re.IGNORECASE)) |
Comment on lines
+585
to
+590
| force_cascade = style in {"cascade", "human_cascade", "human-cascade"} | ||
| if not force_cascade and ( | ||
| (is_group and not self._human_cascade_groups) | ||
| or self._has_approval_gate(text) | ||
| ): | ||
| return self.truncate_message(formatted, limit), False |
Comment on lines
+607
to
+623
| merged_tail = False | ||
| if not force_cascade and self._looks_structured_outbound(text): | ||
| merged = self._merge_structured_tail(paragraphs, max_bubbles) | ||
| if merged is None: | ||
| return self.truncate_message(formatted, limit), False | ||
| paragraphs = merged | ||
| merged_tail = True | ||
|
|
||
| if len(paragraphs) > max_bubbles and not force_cascade: | ||
| # Keep the human cadence without machine-gunning the chat: send the | ||
| # first few thoughts as separate bubbles, then fold the remainder | ||
| # into the final bubble. This handles natural 5–6 paragraph chatty | ||
| # replies better than falling all the way back to one glued block. | ||
| head = paragraphs[: max_bubbles - 1] | ||
| tail = "\n\n".join(paragraphs[max_bubbles - 1:]).strip() | ||
| paragraphs = [*head, tail] | ||
| merged_tail = True |
This reverts commit 14314ba.
Contributor
Author
|
Closing this old branch so we can rebuild the Hermes changes cleanly from current main and reopen smaller fresh PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
single,cascade,auto) for gateway outputsTest Plan
scripts/run_tests.sh tests/gateway/test_whatsapp_formatting.py tests/gateway/test_stream_consumer.py tests/gateway/test_gateway_status_delivery_style.py