Compare commits
No commits in common. "29429d6385f97d8a52371c5058a7520784ca8d91" and "06a8b547abb148c4be1d25fb33ad9d9af95aa230" have entirely different histories.
29429d6385
...
06a8b547ab
5 changed files with 5 additions and 236 deletions
|
|
@ -43,11 +43,9 @@ voice_definition: |
|
||||||
unless the hosted Leo HTTP route returns retained payment/readback evidence.
|
unless the hosted Leo HTTP route returns retained payment/readback evidence.
|
||||||
Ordinary addressed/private chat may be routed into smart research when the
|
Ordinary addressed/private chat may be routed into smart research when the
|
||||||
request clearly asks for sourced, current, market, or evidence-backed work.
|
request clearly asks for sourced, current, market, or evidence-backed work.
|
||||||
Explicit /smart_research remains available for narrow canaries. Quote-first
|
Explicit /smart_research remains available for narrow canaries. Paid smart
|
||||||
paid research should auto-resume in the same Telegram chat after the settled
|
research remains fail-closed unless the server-side allow flag, allowed chat
|
||||||
work order is visible; pasted work_order_id values are fallback/debug only.
|
id, cap, and retained approval-ref file are all present.
|
||||||
Paid smart research remains fail-closed unless the server-side allow flag,
|
|
||||||
allowed chat id, cap, and retained approval-ref file are all present.
|
|
||||||
Do not request or expose private keys, bot tokens, wallet exports, seed phrases,
|
Do not request or expose private keys, bot tokens, wallet exports, seed phrases,
|
||||||
or raw secret values.
|
or raw secret values.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,8 @@ voice_definition: |
|
||||||
payment/readback evidence.
|
payment/readback evidence.
|
||||||
When addressed or used in private chat, clear requests for fresh sourced
|
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 should go through Leo's hosted research route. First return a paid
|
||||||
research quote and checkout link. After payment, continue automatically in
|
research quote and checkout link. Only resume paid execution after a
|
||||||
the same Telegram chat when the settled work order is visible. Treat a
|
work_order_id or payment receipt is present.
|
||||||
work_order_id or payment receipt pasted by the user as a fallback/debug path,
|
|
||||||
not the normal UX.
|
|
||||||
|
|
||||||
# ─── Learnings ───────────────────────────────────────────────────────────
|
# ─── Learnings ───────────────────────────────────────────────────────────
|
||||||
learnings_file: agents/leo/learnings.md
|
learnings_file: agents/leo/learnings.md
|
||||||
|
|
|
||||||
161
telegram/bot.py
161
telegram/bot.py
|
|
@ -55,7 +55,6 @@ from http_chat_proxy import (
|
||||||
DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES,
|
DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES,
|
||||||
build_chat_proxy_payload,
|
build_chat_proxy_payload,
|
||||||
build_smart_research_proxy_payload,
|
build_smart_research_proxy_payload,
|
||||||
classify_smart_research_auto_resume_response,
|
|
||||||
extract_auto_smart_research_followup_goal,
|
extract_auto_smart_research_followup_goal,
|
||||||
extract_auto_smart_research_goal,
|
extract_auto_smart_research_goal,
|
||||||
extract_paid_work_order_id,
|
extract_paid_work_order_id,
|
||||||
|
|
@ -102,7 +101,6 @@ AGENT_RESPOND_TO_PRIVATE_CHATS = False
|
||||||
AGENT_MENTION_ALIASES = ["@teleo", "@FutAIrdBot"]
|
AGENT_MENTION_ALIASES = ["@teleo", "@FutAIrdBot"]
|
||||||
AGENT_SMART_RESEARCH_COMMAND_PREFIXES = list(DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES)
|
AGENT_SMART_RESEARCH_COMMAND_PREFIXES = list(DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES)
|
||||||
AGENT_AUTO_SMART_RESEARCH_FROM_CHAT = False
|
AGENT_AUTO_SMART_RESEARCH_FROM_CHAT = False
|
||||||
smart_research_auto_resume_tasks: set[tuple[int, int]] = set()
|
|
||||||
|
|
||||||
# Rate limits
|
# Rate limits
|
||||||
MAX_RESPONSE_PER_USER_PER_HOUR = 30
|
MAX_RESPONSE_PER_USER_PER_HOUR = 30
|
||||||
|
|
@ -750,145 +748,6 @@ def _smart_research_payment_gate(chat_id: int) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _smart_research_auto_resume_enabled() -> bool:
|
|
||||||
return os.getenv("LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_AUTO_RESUME", "1") != "0"
|
|
||||||
|
|
||||||
|
|
||||||
def _smart_research_auto_resume_attempts() -> int:
|
|
||||||
try:
|
|
||||||
attempts = int(os.getenv("LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_AUTO_RESUME_ATTEMPTS", "24"))
|
|
||||||
except ValueError:
|
|
||||||
return 24
|
|
||||||
return min(max(attempts, 1), 120)
|
|
||||||
|
|
||||||
|
|
||||||
def _smart_research_auto_resume_interval_seconds() -> float:
|
|
||||||
try:
|
|
||||||
interval = float(os.getenv("LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_AUTO_RESUME_INTERVAL_SECONDS", "10"))
|
|
||||||
except ValueError:
|
|
||||||
return 10.0
|
|
||||||
return min(max(interval, 2.0), 120.0)
|
|
||||||
|
|
||||||
|
|
||||||
def _should_start_smart_research_auto_resume_poll(
|
|
||||||
*,
|
|
||||||
paid_work_order_id: str | None,
|
|
||||||
http_status: int,
|
|
||||||
proxy_body: dict,
|
|
||||||
) -> bool:
|
|
||||||
if paid_work_order_id or not _smart_research_auto_resume_enabled():
|
|
||||||
return False
|
|
||||||
if not isinstance(proxy_body, dict):
|
|
||||||
return False
|
|
||||||
if proxy_body.get("status") != "payment_authorization_required":
|
|
||||||
return False
|
|
||||||
checkout = proxy_body.get("checkout")
|
|
||||||
return isinstance(checkout, dict) and bool(checkout.get("checkoutUrl") or checkout.get("checkout_url"))
|
|
||||||
|
|
||||||
|
|
||||||
async def _poll_smart_research_auto_resume(
|
|
||||||
msg,
|
|
||||||
context: ContextTypes.DEFAULT_TYPE,
|
|
||||||
*,
|
|
||||||
research_goal: str,
|
|
||||||
username: str | None,
|
|
||||||
) -> None:
|
|
||||||
"""Poll Leo's chat-bound research route until the x402 work order is visible."""
|
|
||||||
if not AGENT_HTTP_RESEARCH_PROXY_URL:
|
|
||||||
return
|
|
||||||
key = (msg.chat_id, msg.message_id)
|
|
||||||
if key in smart_research_auto_resume_tasks:
|
|
||||||
return
|
|
||||||
smart_research_auto_resume_tasks.add(key)
|
|
||||||
attempts = _smart_research_auto_resume_attempts()
|
|
||||||
interval = _smart_research_auto_resume_interval_seconds()
|
|
||||||
try:
|
|
||||||
for attempt in range(attempts):
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
payload = build_smart_research_proxy_payload(
|
|
||||||
research_goal=research_goal,
|
|
||||||
source="telegram",
|
|
||||||
agent=AGENT_NAME.lower(),
|
|
||||||
chat_id=msg.chat_id,
|
|
||||||
message_id=msg.message_id,
|
|
||||||
username=username,
|
|
||||||
include_synthesis=True,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
status, proxy_body, proxy_reply = await post_chat_proxy(
|
|
||||||
url=AGENT_HTTP_RESEARCH_PROXY_URL,
|
|
||||||
payload=payload,
|
|
||||||
timeout_seconds=90,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.info(
|
|
||||||
"%s smart research auto-resume poll %d/%d failed: %s",
|
|
||||||
AGENT_NAME,
|
|
||||||
attempt + 1,
|
|
||||||
attempts,
|
|
||||||
exc,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
classification = classify_smart_research_auto_resume_response(status, proxy_body)
|
|
||||||
if classification == "pending":
|
|
||||||
continue
|
|
||||||
if classification == "ready" and proxy_reply:
|
|
||||||
await _reply_text_native(msg, proxy_reply, do_quote=True)
|
|
||||||
if msg.from_user:
|
|
||||||
entry = {
|
|
||||||
"user": research_goal[:500],
|
|
||||||
"bot": proxy_reply[:500],
|
|
||||||
"username": username or "anonymous",
|
|
||||||
}
|
|
||||||
user_key = (msg.chat_id, msg.from_user.id)
|
|
||||||
history = conversation_history.setdefault(user_key, [])
|
|
||||||
history.append(entry)
|
|
||||||
if len(history) > MAX_HISTORY_USER:
|
|
||||||
history.pop(0)
|
|
||||||
chat_key = (msg.chat_id, 0)
|
|
||||||
chat_history = conversation_history.setdefault(chat_key, [])
|
|
||||||
chat_history.append(entry)
|
|
||||||
if len(chat_history) > MAX_HISTORY_CHAT:
|
|
||||||
chat_history.pop(0)
|
|
||||||
user_response_times[msg.from_user.id].append(time.time())
|
|
||||||
_record_transcript(
|
|
||||||
msg,
|
|
||||||
proxy_reply,
|
|
||||||
is_bot=True,
|
|
||||||
rio_response=proxy_reply,
|
|
||||||
internal={
|
|
||||||
"agent": AGENT_NAME.lower(),
|
|
||||||
"http_research_proxy_auto_resume": True,
|
|
||||||
"http_status": status,
|
|
||||||
"schema": proxy_body.get("schema") if isinstance(proxy_body, dict) else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"%s auto-resumed paid smart research in Telegram chat_id=%s message_id=%s",
|
|
||||||
AGENT_NAME,
|
|
||||||
msg.chat_id,
|
|
||||||
msg.message_id,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
logger.info(
|
|
||||||
"%s smart research auto-resume poll stopped on classification=%s status=%s",
|
|
||||||
AGENT_NAME,
|
|
||||||
classification,
|
|
||||||
status,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
logger.info(
|
|
||||||
"%s smart research auto-resume poll expired chat_id=%s message_id=%s attempts=%s",
|
|
||||||
AGENT_NAME,
|
|
||||||
msg.chat_id,
|
|
||||||
msg.message_id,
|
|
||||||
attempts,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
smart_research_auto_resume_tasks.discard(key)
|
|
||||||
|
|
||||||
|
|
||||||
async def _market_context_for_message(
|
async def _market_context_for_message(
|
||||||
text: str,
|
text: str,
|
||||||
extra_terms: list[str] | tuple[str, ...] = (),
|
extra_terms: list[str] | tuple[str, ...] = (),
|
||||||
|
|
@ -1420,26 +1279,6 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
|
||||||
await _reply_text_native(msg, proxy_reply, do_quote=True)
|
await _reply_text_native(msg, proxy_reply, do_quote=True)
|
||||||
|
|
||||||
if _should_start_smart_research_auto_resume_poll(
|
|
||||||
paid_work_order_id=paid_work_order_id,
|
|
||||||
http_status=status,
|
|
||||||
proxy_body=proxy_body,
|
|
||||||
):
|
|
||||||
context.application.create_task(
|
|
||||||
_poll_smart_research_auto_resume(
|
|
||||||
msg,
|
|
||||||
context,
|
|
||||||
research_goal=smart_research_goal,
|
|
||||||
username=user.username if user else None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"%s started smart research auto-resume poll chat_id=%s message_id=%s",
|
|
||||||
AGENT_NAME,
|
|
||||||
msg.chat_id,
|
|
||||||
msg.message_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
username = user.username or "anonymous"
|
username = user.username or "anonymous"
|
||||||
key = (msg.chat_id, user.id)
|
key = (msg.chat_id, user.id)
|
||||||
|
|
|
||||||
|
|
@ -197,35 +197,6 @@ def smart_research_payment_fields_for_message(
|
||||||
return {"allow_paid_execution": False}
|
return {"allow_paid_execution": False}
|
||||||
|
|
||||||
|
|
||||||
def classify_smart_research_auto_resume_response(
|
|
||||||
http_status: int,
|
|
||||||
payload: dict[str, Any] | None,
|
|
||||||
) -> str:
|
|
||||||
"""Classify a poll response for Telegram auto-resume.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- pending: the payment/work order is not visible yet; keep polling silently.
|
|
||||||
- ready: the route has a final user-facing reply or a visible paid-work-order blocker.
|
|
||||||
- ignore: the response is unrelated to an auto-resume poll.
|
|
||||||
"""
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
return "ignore"
|
|
||||||
status = str(payload.get("status") or "").strip()
|
|
||||||
auto_resume = payload.get("autoResume")
|
|
||||||
paid_work_order = payload.get("paidWorkOrder")
|
|
||||||
auto_activated = isinstance(auto_resume, dict) and auto_resume.get("activated") is True
|
|
||||||
work_order_settled = (
|
|
||||||
isinstance(paid_work_order, dict) and paid_work_order.get("status") == "settled"
|
|
||||||
)
|
|
||||||
if status == "payment_authorization_required" and not auto_activated and not work_order_settled:
|
|
||||||
return "pending"
|
|
||||||
if auto_activated or work_order_settled:
|
|
||||||
return "ready"
|
|
||||||
if http_status < 400 and status and status != "payment_authorization_required":
|
|
||||||
return "ready"
|
|
||||||
return "ignore"
|
|
||||||
|
|
||||||
|
|
||||||
def build_smart_research_proxy_payload(
|
def build_smart_research_proxy_payload(
|
||||||
*,
|
*,
|
||||||
research_goal: str,
|
research_goal: str,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ from agent_config import load_agent_config # noqa: E402
|
||||||
from http_chat_proxy import ( # noqa: E402
|
from http_chat_proxy import ( # noqa: E402
|
||||||
build_chat_proxy_payload,
|
build_chat_proxy_payload,
|
||||||
build_smart_research_proxy_payload,
|
build_smart_research_proxy_payload,
|
||||||
classify_smart_research_auto_resume_response,
|
|
||||||
extract_auto_smart_research_followup_goal,
|
extract_auto_smart_research_followup_goal,
|
||||||
extract_auto_smart_research_goal,
|
extract_auto_smart_research_goal,
|
||||||
extract_paid_work_order_id,
|
extract_paid_work_order_id,
|
||||||
|
|
@ -170,42 +169,6 @@ def test_smart_research_payment_fields_resume_uses_capped_gate():
|
||||||
assert payment_fields == configured_gate
|
assert payment_fields == configured_gate
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("http_status", "payload", "expected"),
|
|
||||||
[
|
|
||||||
(
|
|
||||||
402,
|
|
||||||
{
|
|
||||||
"status": "payment_authorization_required",
|
|
||||||
"autoResume": {"activated": False},
|
|
||||||
},
|
|
||||||
"pending",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
"status": "source_only_done",
|
|
||||||
"autoResume": {"activated": True},
|
|
||||||
"paidWorkOrder": {"status": "settled"},
|
|
||||||
},
|
|
||||||
"ready",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
402,
|
|
||||||
{
|
|
||||||
"status": "payment_authorization_required",
|
|
||||||
"autoResume": {"activated": True},
|
|
||||||
"paidWorkOrder": {"status": "settled"},
|
|
||||||
},
|
|
||||||
"ready",
|
|
||||||
),
|
|
||||||
(500, {"error": "temporary"}, "ignore"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_classify_smart_research_auto_resume_response(http_status, payload, expected):
|
|
||||||
assert classify_smart_research_auto_resume_response(http_status, payload) == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("message", "expected"),
|
("message", "expected"),
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue