Merge pull request #24 from living-ip/codex/leo-telegram-x402-quote-resume-20260630
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Enable quote-first Leo Telegram research
This commit is contained in:
commit
06a8b547ab
4 changed files with 66 additions and 2 deletions
|
|
@ -19,6 +19,11 @@ domain_expertise: >
|
||||||
|
|
||||||
# ─── Hosted Leo Runtime ──────────────────────────────────────────────────
|
# ─── Hosted Leo Runtime ──────────────────────────────────────────────────
|
||||||
http_chat_proxy_url: "https://leo.livingip.xyz/api/agents/leo/chat"
|
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
|
respond_to_private_chats: true
|
||||||
|
|
||||||
# ─── KB Scope ────────────────────────────────────────────────────────────
|
# ─── KB Scope ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -44,6 +49,10 @@ voice_definition: |
|
||||||
answer from retained Living IP runtime evidence and current route state.
|
answer from retained Living IP runtime evidence and current route state.
|
||||||
Do not claim payment execution unless the HTTP route returns retained
|
Do not claim payment execution unless the HTTP route returns retained
|
||||||
payment/readback evidence.
|
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 ───────────────────────────────────────────────────────────
|
||||||
learnings_file: agents/leo/learnings.md
|
learnings_file: agents/leo/learnings.md
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ from http_chat_proxy import (
|
||||||
extract_smart_research_goal,
|
extract_smart_research_goal,
|
||||||
post_chat_proxy,
|
post_chat_proxy,
|
||||||
should_attach_structured_market_context,
|
should_attach_structured_market_context,
|
||||||
|
smart_research_payment_fields_for_message,
|
||||||
smart_research_command_names,
|
smart_research_command_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1128,6 +1129,10 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if len(text) < MIN_MESSAGE_LENGTH:
|
if len(text) < MIN_MESSAGE_LENGTH:
|
||||||
return
|
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)
|
# Conversation window behavior depends on chat type (Rio: DMs vs groups)
|
||||||
# DMs: auto-respond (always 1-on-1, no false positives)
|
# DMs: auto-respond (always 1-on-1, no false positives)
|
||||||
# Groups: silent context only (reply-to is the only follow-up trigger)
|
# 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),
|
tuple(AGENT_MENTION_ALIASES),
|
||||||
)
|
)
|
||||||
if AGENT_HTTP_RESEARCH_PROXY_URL and smart_research_goal:
|
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
|
proxy_research_goal = smart_research_goal
|
||||||
if should_attach_structured_market_context(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(
|
market_context, market_data_audit, market_duration, market_tokens = await _market_context_for_message(
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,19 @@ def should_attach_structured_market_context(message: str) -> bool:
|
||||||
return bool(_MARKET_CONTEXT_RE.search(text))
|
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(
|
def build_smart_research_proxy_payload(
|
||||||
*,
|
*,
|
||||||
research_goal: str,
|
research_goal: str,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from http_chat_proxy import ( # noqa: E402
|
||||||
extract_chat_proxy_reply,
|
extract_chat_proxy_reply,
|
||||||
extract_smart_research_goal,
|
extract_smart_research_goal,
|
||||||
should_attach_structured_market_context,
|
should_attach_structured_market_context,
|
||||||
|
smart_research_payment_fields_for_message,
|
||||||
smart_research_command_names,
|
smart_research_command_names,
|
||||||
)
|
)
|
||||||
from market_data import extract_market_data_tokens, format_price_context # noqa: E402
|
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.name == "Leo"
|
||||||
assert leo.http_chat_proxy_url == "https://leo.livingip.xyz/api/agents/leo/chat"
|
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 leo.respond_to_private_chats is True
|
||||||
assert "@teLEOhuman" in leo.mention_aliases
|
assert "@teLEOhuman" in leo.mention_aliases
|
||||||
assert leo_wallet_test.name == "Leo Wallet Test"
|
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 "@lipleowallet0622183538bot" in leo_wallet_test.mention_aliases
|
||||||
assert rio.http_chat_proxy_url is None
|
assert rio.http_chat_proxy_url is None
|
||||||
assert rio.respond_to_private_chats is False
|
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):
|
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()
|
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(
|
@pytest.mark.parametrize(
|
||||||
("message", "expected"),
|
("message", "expected"),
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue