From a9a90f0faf65acfab3ef225daa596b7a36cc47d0 Mon Sep 17 00:00:00 2001 From: twentyOne2x Date: Wed, 1 Jul 2026 21:35:29 +0200 Subject: [PATCH] Split paid research acknowledgement from answer --- telegram/bot.py | 19 ++++++++++--- telegram/http_chat_proxy.py | 39 ++++++++++++++++++++++++++ tests/test_telegram_leo_x402_bridge.py | 30 ++++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 97252f8..475d4a8 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -65,6 +65,7 @@ from http_chat_proxy import ( should_attach_structured_market_context, smart_research_payment_fields_for_message, smart_research_command_names, + split_smart_research_auto_resume_reply, ) # ─── Config ───────────────────────────────────────────────────────────── @@ -859,11 +860,21 @@ async def _poll_smart_research_auto_resume( if classification == "pending": continue if classification == "ready" and proxy_reply: - await _reply_text_native(msg, proxy_reply, do_quote=True) + ack_reply, answer_reply = split_smart_research_auto_resume_reply( + proxy_body, + proxy_reply, + ) + if ack_reply: + await _reply_text_native(msg, ack_reply, do_quote=True) + if answer_reply: + await _reply_text_native(msg, answer_reply, do_quote=not ack_reply) + visible_reply = "\n\n".join( + part for part in (ack_reply, answer_reply) if part + ) or proxy_reply if msg.from_user: entry = { "user": research_goal[:500], - "bot": proxy_reply[:500], + "bot": visible_reply[:500], "username": username or "anonymous", } user_key = (msg.chat_id, msg.from_user.id) @@ -879,9 +890,9 @@ async def _poll_smart_research_auto_resume( user_response_times[msg.from_user.id].append(time.time()) _record_transcript( msg, - proxy_reply, + visible_reply, is_bot=True, - rio_response=proxy_reply, + rio_response=visible_reply, internal={ "agent": AGENT_NAME.lower(), "http_research_proxy_auto_resume": True, diff --git a/telegram/http_chat_proxy.py b/telegram/http_chat_proxy.py index 4f55f4d..9dcfce6 100644 --- a/telegram/http_chat_proxy.py +++ b/telegram/http_chat_proxy.py @@ -7,7 +7,15 @@ from html import escape from typing import Any DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES = ("/smart_research", "/paid_research") +SMART_RESEARCH_PAYMENT_ACK_TEXT = "Payment received. Research is starting now." _TELEGRAM_COMMAND_NAME_RE = re.compile(r"^[A-Za-z0-9_]+$") +_SMART_RESEARCH_PAYMENT_ACK_PREFIX_RE = re.compile( + r"^\s*" + r"Payment received[.!]\s*" + r"(?:Research is starting now[.!]\s*)?" + r"(?:I will use the included source-tool budget and continue with the answer here[.!]\s*)?", + re.IGNORECASE, +) _AUTO_SMART_RESEARCH_RE = re.compile( r"\b(" r"research|source|sources|citation|citations|evidence|" @@ -319,6 +327,37 @@ def classify_smart_research_auto_resume_response( return "ignore" +def split_smart_research_auto_resume_reply( + payload: dict[str, Any] | None, + reply: str | None, +) -> tuple[str | None, str | None]: + """Split a paid auto-resume reply into receipt ack and answer text. + + The HTTP route may return one combined reply starting with "Payment + received...". Telegram should show that as two messages: a quick payment + receipt first, then the research result or clean tool-failure message. + """ + if not isinstance(reply, str) or not reply.strip(): + return None, None + text = reply.strip() + if not isinstance(payload, dict): + return None, text + + auto_resume = payload.get("autoResume") + paid_work_order = payload.get("paidWorkOrder") + paid_resume = ( + isinstance(auto_resume, dict) + and auto_resume.get("activated") is True + or isinstance(paid_work_order, dict) + and paid_work_order.get("status") == "settled" + ) + if not paid_resume: + return None, text + + answer = _SMART_RESEARCH_PAYMENT_ACK_PREFIX_RE.sub("", text, count=1).strip() + return SMART_RESEARCH_PAYMENT_ACK_TEXT, answer or None + + def build_smart_research_proxy_payload( *, research_goal: str, diff --git a/tests/test_telegram_leo_x402_bridge.py b/tests/test_telegram_leo_x402_bridge.py index d2e6f28..6b170be 100644 --- a/tests/test_telegram_leo_x402_bridge.py +++ b/tests/test_telegram_leo_x402_bridge.py @@ -23,6 +23,7 @@ from http_chat_proxy import ( # noqa: E402 extract_chat_proxy_reply, extract_smart_research_goal, should_attach_structured_market_context, + split_smart_research_auto_resume_reply, smart_research_payment_fields_for_message, smart_research_command_names, ) @@ -227,6 +228,35 @@ def test_classify_smart_research_auto_resume_response(http_status, payload, expe assert classify_smart_research_auto_resume_response(http_status, payload) == expected +def test_split_smart_research_auto_resume_reply_separates_payment_ack_from_answer(): + payload = { + "autoResume": {"activated": True}, + "paidWorkOrder": {"status": "settled"}, + } + reply = ( + "Payment received. Research is starting now. " + "I will use the included source-tool budget and continue with the answer here.\n\n" + "Based on the Twitter/X results I found, Ranger is being discussed as wound down." + ) + + ack, answer = split_smart_research_auto_resume_reply(payload, reply) + + assert ack == "Payment received. Research is starting now." + assert answer == ( + "Based on the Twitter/X results I found, Ranger is being discussed as wound down." + ) + + +def test_split_smart_research_auto_resume_reply_keeps_non_paid_ready_reply_intact(): + ack, answer = split_smart_research_auto_resume_reply( + {"status": "complete"}, + "Here is the answer.", + ) + + assert ack is None + assert answer == "Here is the answer." + + @pytest.mark.parametrize( ("message", "expected"), [