diff --git a/telegram/agents/leo.yaml b/telegram/agents/leo.yaml index 860bdf7..9b35588 100644 --- a/telegram/agents/leo.yaml +++ b/telegram/agents/leo.yaml @@ -19,6 +19,11 @@ domain_expertise: > # ─── Hosted Leo Runtime ────────────────────────────────────────────────── http_chat_proxy_url: "https://leo.livingip.xyz/api/agents/leo/chat" +http_research_proxy_url: "https://leo.livingip.xyz/api/agents/leo/research" +smart_research_command_prefixes: + - "/smart_research" + - "/paid_research" +auto_smart_research_from_chat: true respond_to_private_chats: true # ─── KB Scope ──────────────────────────────────────────────────────────── @@ -44,6 +49,10 @@ voice_definition: | answer from retained Living IP runtime evidence and current route state. Do not claim payment execution unless the HTTP route returns retained payment/readback evidence. + When addressed or used in private chat, clear requests for fresh sourced + research should go through Leo's hosted research route. First return a paid + research quote and checkout link. Only resume paid execution after a + work_order_id or payment receipt is present. # ─── Learnings ─────────────────────────────────────────────────────────── learnings_file: agents/leo/learnings.md diff --git a/telegram/bot.py b/telegram/bot.py index 594988e..2d4a053 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -61,6 +61,7 @@ from http_chat_proxy import ( extract_smart_research_goal, post_chat_proxy, should_attach_structured_market_context, + smart_research_payment_fields_for_message, smart_research_command_names, ) @@ -1128,6 +1129,10 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): if len(text) < MIN_MESSAGE_LENGTH: return + if AGENT_HTTP_RESEARCH_PROXY_URL and extract_paid_work_order_id(text): + await handle_tagged(update, context) + return + # Conversation window behavior depends on chat type (Rio: DMs vs groups) # DMs: auto-respond (always 1-on-1, no false positives) # Groups: silent context only (reply-to is the only follow-up trigger) @@ -1207,7 +1212,10 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE): tuple(AGENT_MENTION_ALIASES), ) if AGENT_HTTP_RESEARCH_PROXY_URL and smart_research_goal: - payment_gate = _smart_research_payment_gate(msg.chat_id) + payment_gate = smart_research_payment_fields_for_message( + paid_work_order_id=paid_work_order_id, + configured_payment_gate=_smart_research_payment_gate(msg.chat_id), + ) proxy_research_goal = smart_research_goal if should_attach_structured_market_context(smart_research_goal): market_context, market_data_audit, market_duration, market_tokens = await _market_context_for_message( diff --git a/telegram/http_chat_proxy.py b/telegram/http_chat_proxy.py index ad71a5a..3587b5c 100644 --- a/telegram/http_chat_proxy.py +++ b/telegram/http_chat_proxy.py @@ -184,6 +184,19 @@ def should_attach_structured_market_context(message: str) -> bool: return bool(_MARKET_CONTEXT_RE.search(text)) +def smart_research_payment_fields_for_message( + *, + paid_work_order_id: str | None, + configured_payment_gate: dict[str, Any], +) -> dict[str, Any]: + """Return paid-execution fields only for a settled work-order resume message.""" + if not paid_work_order_id: + return {"allow_paid_execution": False} + if configured_payment_gate.get("allow_paid_execution") is True: + return configured_payment_gate + return {"allow_paid_execution": False} + + 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 2ba8412..96e424d 100644 --- a/tests/test_telegram_leo_x402_bridge.py +++ b/tests/test_telegram_leo_x402_bridge.py @@ -19,6 +19,7 @@ from http_chat_proxy import ( # noqa: E402 extract_chat_proxy_reply, extract_smart_research_goal, should_attach_structured_market_context, + smart_research_payment_fields_for_message, smart_research_command_names, ) from market_data import extract_market_data_tokens, format_price_context # noqa: E402 @@ -31,6 +32,9 @@ def test_leo_config_opts_into_http_chat_proxy_without_changing_default_agents(): assert leo.name == "Leo" assert leo.http_chat_proxy_url == "https://leo.livingip.xyz/api/agents/leo/chat" + assert leo.http_research_proxy_url == "https://leo.livingip.xyz/api/agents/leo/research" + assert "/smart_research" in leo.smart_research_command_prefixes + assert leo.auto_smart_research_from_chat is True assert leo.respond_to_private_chats is True assert "@teLEOhuman" in leo.mention_aliases assert leo_wallet_test.name == "Leo Wallet Test" @@ -43,7 +47,6 @@ def test_leo_config_opts_into_http_chat_proxy_without_changing_default_agents(): assert "@lipleowallet0622183538bot" in leo_wallet_test.mention_aliases assert rio.http_chat_proxy_url is None assert rio.respond_to_private_chats is False - assert leo.auto_smart_research_from_chat is False def test_invalid_http_chat_proxy_url_fails_closed(tmp_path): @@ -135,6 +138,37 @@ def test_smart_research_payload_can_resume_paid_work_order_without_secret_materi assert "secret" not in str(payload).lower() +def test_smart_research_payment_fields_quote_first_even_when_gate_enabled(): + configured_gate = { + "allow_paid_execution": True, + "approval_ref": "approval_ref_livingip_x402_20260622", + "max_amount_usd": 0.01, + } + + payment_fields = smart_research_payment_fields_for_message( + paid_work_order_id=None, + configured_payment_gate=configured_gate, + ) + + assert payment_fields == {"allow_paid_execution": False} + assert "approval_ref" not in payment_fields + + +def test_smart_research_payment_fields_resume_uses_capped_gate(): + configured_gate = { + "allow_paid_execution": True, + "approval_ref": "approval_ref_livingip_x402_20260622", + "max_amount_usd": 0.01, + } + + payment_fields = smart_research_payment_fields_for_message( + paid_work_order_id="sponsored_work_orders:f951ccc6c7762ecba6f76cf6", + configured_payment_gate=configured_gate, + ) + + assert payment_fields == configured_gate + + @pytest.mark.parametrize( ("message", "expected"), [