Merge pull request #3 from living-ip/leo-telegram-x402-bridge

Add Leo Telegram x402 bridge
This commit is contained in:
twentyOne2x 2026-06-19 19:36:04 +02:00 committed by GitHub
commit 20f85f94dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 692 additions and 6 deletions

View file

@ -13,6 +13,7 @@ fi
DEPLOY_CHECKOUT="/opt/teleo-eval/workspaces/deploy-infra"
PIPELINE_DIR="/opt/teleo-eval/pipeline"
TELEGRAM_DIR="/opt/teleo-eval/telegram"
DIAGNOSTICS_DIR="/opt/teleo-eval/diagnostics"
AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state"
STAMP_FILE="/opt/teleo-eval/.last-deploy-sha"
@ -74,6 +75,7 @@ for f in teleo-pipeline.py reweave.py fetch_coins.py pipeline-health-check.py; d
done
rsync "${RSYNC_OPTS[@]}" telegram/ "$PIPELINE_DIR/telegram/"
rsync "${RSYNC_OPTS[@]}" telegram/ "$TELEGRAM_DIR/"
rsync "${RSYNC_OPTS[@]}" diagnostics/ "$DIAGNOSTICS_DIR/"
rsync "${RSYNC_OPTS[@]}" agent-state/ "$AGENT_STATE_DIR/"
rsync "${RSYNC_OPTS[@]}" tests/ "$PIPELINE_DIR/tests/"

View file

@ -7,6 +7,7 @@ set -euo pipefail
VPS_HOST="teleo@77.42.65.182"
VPS_PIPELINE="/opt/teleo-eval/pipeline"
VPS_TELEGRAM="/opt/teleo-eval/telegram"
VPS_DIAGNOSTICS="/opt/teleo-eval/diagnostics"
VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@ -74,6 +75,7 @@ echo ""
echo "=== Telegram bot ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_PIPELINE/telegram/"
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_TELEGRAM/"
echo ""
echo "=== Tests ==="

View file

@ -0,0 +1,21 @@
{
"agent": "leo",
"currentTier": "T3_live_readonly",
"generatedAt": "2026-06-19T17:25:27.555494+00:00",
"httpStatus": 402,
"llmOk": true,
"notProven": [
"teleo-agent@leo.service active",
"Telegram message delivery",
"Telegram reply delivery",
"new payment execution"
],
"ok": true,
"reply": "This reached Leo HTTP via Telegram bridge confirmation.",
"requiredTier": "T3_live_readonly",
"routeSchema": "livingip.x402.leoChatResponse.v1",
"schema": "livingip.telegramLeoX402BridgeProof.v1",
"secretValuesIncluded": false,
"strongestClaimAllowed": "Telegram bridge helper can POST a no-secret payload to the public Leo HTTP chat route and extract a usable Leo reply. This proves the bridge parser/readback only; it does not prove the Telegram bot service is deployed or active.",
"url": "https://leo.livingip.xyz/api/agents/leo/chat"
}

View file

@ -0,0 +1,83 @@
# Telegram Leo x402 Bridge PR Packet
## Working Target
Run Leo as a Telegram bot without duplicating Leo/x402 logic: Telegram receives
a user message, forwards it to `https://leo.livingip.xyz/api/agents/leo/chat`,
and replies with the hosted Leo answer.
## Non-Destructive Boundary
- This PR does not start, stop, restart, or mutate any live Telegram service.
- Deployment sync is updated to copy `telegram/` into both
`/opt/teleo-eval/pipeline/telegram/` and `/opt/teleo-eval/telegram/`, matching
the current `teleo-agent@.service` runtime path.
- Existing Rio and Theseus configs do not set `http_chat_proxy_url`, so their
current KB/retrieval path stays unchanged.
- Leo opts into the bridge with `telegram/agents/leo.yaml`.
- The live token's Telegram username readback is `@TeleoHumanBot`; `@teLEOhuman`
remains an alias for continuity with Leo's X identity.
- Secret contents are not stored or printed. The config references only the
expected token-file name: `leo-telegram-bot-token`.
## Local Proof Commands
```sh
.venv/bin/python -m pytest tests/test_telegram_leo_x402_bridge.py
.venv/bin/python -m py_compile telegram/agent_config.py telegram/http_chat_proxy.py telegram/bot.py telegram/agent_runner.py
.venv/bin/python telegram/agent_runner.py --agent leo --validate
.venv/bin/python scripts/check_telegram_leo_x402_bridge.py
bash -n deploy/deploy.sh deploy/auto-deploy.sh
git diff --check
```
Primary retained proof path:
```text
docs/reports/telegram-leo-x402-bridge-proof.json
```
## Production Promotion Commands
Run only after review and after confirming the token filename exists on the VPS:
```sh
test -f /opt/teleo-eval/secrets/leo-telegram-bot-token
test -f /opt/teleo-eval/telegram/agents/leo.yaml
test -f /opt/teleo-eval/telegram/http_chat_proxy.py
/opt/teleo-eval/pipeline/.venv/bin/python3 /opt/teleo-eval/telegram/agent_runner.py --agent leo --validate
systemctl start teleo-agent@leo
journalctl -u teleo-agent@leo -n 100 --no-pager
```
Then send Leo a Telegram DM or tag the configured handle and retain:
- Telegram message/reply screenshot or export.
- `journalctl -u teleo-agent@leo` lines showing the proxy path.
- Caddy access log line for `POST /api/agents/leo/chat` on `leo.livingip.xyz`.
## Reviewer CTA
Approve deploying this as the next non-destructive Telegram step if these facts
are acceptable:
- `leo-telegram-bot-token` exists on the VPS.
- Telegram `getMe` for that token reports bot username `TeleoHumanBot`.
- `teleo-agent@leo.service` is currently inactive, so this is an additive new
agent process rather than a restart of Rio or Theseus.
- The public Leo HTTP route already returns a parseable Leo reply.
- Existing Rio/Theseus configs do not set `http_chat_proxy_url`.
- The deploy-path mismatch is fixed by syncing Telegram files to the runtime
path used by `teleo-agent@.service`.
## Strongest Claim Before Promotion
PR-ready local bridge only: config and parser tests prove Telegram can be wired
to the hosted Leo HTTP route without changing existing Rio/Theseus behavior.
## Strongest Claim After Promotion
If the production commands pass and a Telegram message returns a hosted Leo
answer, Telegram Leo is a live transport for Leo's public HTTP chat route.
Payment and external research claims still come from retained HTTP/x402 proof
artifacts, not from Telegram by itself.

View file

@ -0,0 +1,133 @@
# Telegram Leo x402 Priority And Spec
## Definition Of Working
Working target: a user can DM or tag `@TeleoHumanBot`; the Telegram Leo process
forwards the message to `https://leo.livingip.xyz/api/agents/leo/chat`; the user
receives a Leo answer; retained logs prove the request hit the public Leo HTTP
route.
Operator path:
```sh
/opt/teleo-eval/pipeline/.venv/bin/python3 /opt/teleo-eval/telegram/agent_runner.py --agent leo --validate
systemctl start teleo-agent@leo
journalctl -u teleo-agent@leo -n 100 --no-pager
```
Done means:
- `teleo-agent@leo.service` is active on `77.42.65.182`.
- A real Telegram message to `@TeleoHumanBot` receives a Leo reply.
- Retained proof includes Telegram message/readback, `journalctl` proxy log, and
`leo.livingip.xyz` HTTP access/readback.
- Rio and Theseus remain unaffected.
Not done:
- HTTP-only proof without a live Telegram delivery.
- Candidate/local proof without the public bot service active.
- Payment evidence reused as Telegram delivery evidence.
Required tier: `T3_live_readonly` for the Telegram transport; payment claims use
the separately retained x402/Faremeter/AgentCash evidence tiers.
Current tier: `T3_live_readonly` for bridge-to-public-HTTP proof only. The bot
token exists on the VPS, `getMe` identifies `@TeleoHumanBot`, and temporary VPS
config validation passed. The live `teleo-agent@leo.service` deployment has not
been started by this PR-shaped patch.
Promotion gate: current VPS readback showed `teleo-agent@leo.service` uses
`/opt/teleo-eval/telegram/agent_runner.py`, while deploy scripts historically
synced `telegram/` only into `/opt/teleo-eval/pipeline/telegram/`. This patch
updates both manual and auto deploy scripts to sync `telegram/` into the runtime
path too. Do not start `teleo-agent@leo` until `leo.yaml` and
`http_chat_proxy.py` read back from `/opt/teleo-eval/telegram/`.
## Priority Matrix
| Priority | Lane | Current State | Ship Decision |
| --- | --- | --- | --- |
| P0 | Telegram Leo bridge deploy/readback | PR-shaped patch exists; public HTTP proof is retained; VPS token and config validation are confirmed; deploy-path mismatch is patched locally. | Push/merge the bridge, confirm runtime files read back under `/opt/teleo-eval/telegram`, start `teleo-agent@leo`, and retain Telegram delivery logs. |
| P0 | Self-hosted Faremeter seller rail | Retained public and hosted mainnet canary receipts exist, and direct `77.42.65.182:3118` currently serves a valid 0.01 USDC mainnet challenge. Fresh `https://leo.livingip.xyz` readback currently returns a Devnet `payment_challenge_unavailable` response, so public host routing is not proving the mainnet Faremeter rail right now. | Keep Faremeter as the default seller rail, but repair/repoint public `leo.livingip.xyz` to the working mainnet route before claiming current public mainnet seller readiness. |
| P1 | Leo paid research outbound loop | AgentCash/StableEnrich paid answer and Leo analysis proof already exist. | Expose the result through Telegram after bridge deploy; add per-provider approval packets for new services. |
| P1 | Public Leo HTTP behavior | `https://leo.livingip.xyz/api/agents/leo/chat` returns a parseable Leo reply under the current schema. | Treat as the bridge backend; avoid duplicating Leo logic inside Telegram. |
| P2 | Corbits/Herd/payable external services | Corbits moved payment but failed upstream API-key validation; Herd still needs an authenticated/payable endpoint proof. | Keep as provider-specific follow-up; do not block Telegram/Faremeter shipping on it. |
| P2 | All inbound service coverage | Sponsor-research has the strongest retained x402 receipts; other catalog rows need per-service canaries. | Broaden after Telegram bridge is live. |
## Spec Tickets
### TLG-001: Merge And Deploy Telegram Leo Bridge
Surface: `telegram/agent_config.py`, `telegram/bot.py`,
`telegram/http_chat_proxy.py`, `telegram/agents/leo.yaml`.
Acceptance:
- Deploy scripts sync `telegram/` into `/opt/teleo-eval/telegram/`, matching
`teleo-agent@.service`.
- Leo config validates in the production venv.
- `teleo-agent@leo.service` starts without restarting Rio or Theseus.
- A Telegram DM/tag reaches the HTTP proxy branch.
- Failure from the HTTP route returns a clear fail-closed Telegram response.
Evidence:
- `docs/reports/telegram-leo-x402-bridge-proof.json`
- `journalctl -u teleo-agent@leo -n 100 --no-pager`
- Telegram screenshot/export for the delivered reply.
### TLG-002: Retain Live Telegram Proof
Surface: `scripts/check_telegram_leo_x402_bridge.py` plus a live deployment
proof artifact after promotion.
Acceptance:
- Proof names the public Telegram bot handle and public Leo HTTP URL.
- Proof says whether the message was Telegram-delivered or HTTP-only.
- Proof includes no token values, secrets, chat-private content beyond the test
prompt and Leo reply.
### X402-FARE-001: Make Faremeter The Default Seller Rail
Surface: Living IP x402 route configuration and operator docs in the x402
worktree.
Acceptance:
- Public sponsor-research route keeps using the self-hosted Faremeter path.
- Fresh public readback for `https://leo.livingip.xyz/api/initiatives/sponsor-research`
returns the intended mainnet 0.01 USDC challenge, not the stale Devnet
`payment_challenge_unavailable` response.
- A repeat public canary command is documented with the smallest safe spend cap.
- No PayAI/CDP dependency is required for the default seller rail.
Existing evidence:
- `ops/x402-faremeter-mainnet-public-payment-proof.json`
- `ops/x402-faremeter-hosted-candidate-payment-proof.json`
- `ops/x402-faremeter-direct-payment-proof.json`
### LEO-OUT-001: Telegram Surface For Paid Research Results
Surface: Telegram Leo bridge plus retained paid-source artifacts.
Acceptance:
- Telegram Leo can answer a question using the same public Leo HTTP behavior
that already consumed paid AgentCash research.
- The answer references retained paid-source evidence without claiming a fresh
payment unless a fresh payment receipt exists.
Existing evidence:
- `ops/x402-agentcash-paid-readback-proof.json`
- `ops/x402-leo-paid-research-analysis-proof.json`
## Reviewer CTA
Approve the PR-shaped Telegram bridge and then run the production promotion
commands from `docs/telegram-leo-x402-bridge-pr-packet.md`. Do not wait on
Corbits/Herd broadening to ship the Telegram transport and self-hosted Faremeter
seller rail.

View file

@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Prove the Telegram Leo bridge can consume the public Leo HTTP chat route."""
# ruff: noqa: E402, I001
from __future__ import annotations
import argparse
import asyncio
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
TELEGRAM_DIR = REPO_ROOT / "telegram"
sys.path.insert(0, str(TELEGRAM_DIR))
from http_chat_proxy import build_chat_proxy_payload, post_chat_proxy
DEFAULT_URL = "https://leo.livingip.xyz/api/agents/leo/chat"
DEFAULT_OUTPUT = "docs/reports/telegram-leo-x402-bridge-proof.json"
async def run_check(url: str, message: str) -> dict:
payload = build_chat_proxy_payload(
message=message,
source="telegram-proof",
agent="leo",
chat_id=0,
message_id=0,
username="codex-proof",
)
status, body, reply = await post_chat_proxy(url=url, payload=payload)
return {
"schema": "livingip.telegramLeoX402BridgeProof.v1",
"generatedAt": datetime.now(timezone.utc).isoformat(),
"ok": bool(reply),
"requiredTier": "T3_live_readonly",
"currentTier": "T3_live_readonly" if reply else "T2_runtime",
"url": url,
"httpStatus": status,
"routeSchema": body.get("schema") if isinstance(body, dict) else None,
"agent": body.get("agent") if isinstance(body, dict) else None,
"llmOk": (
body.get("llmOk")
if isinstance(body, dict) and "llmOk" in body
else body.get("llm", {}).get("ok")
if isinstance(body.get("llm"), dict)
else None
),
"reply": reply,
"secretValuesIncluded": False,
"strongestClaimAllowed": (
"Telegram bridge helper can POST a no-secret payload to the public Leo HTTP chat route "
"and extract a usable Leo reply. This proves the bridge parser/readback only; it does "
"not prove the Telegram bot service is deployed or active."
),
"notProven": [
"teleo-agent@leo.service active",
"Telegram message delivery",
"Telegram reply delivery",
"new payment execution",
],
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--url", default=DEFAULT_URL)
parser.add_argument(
"--message",
default="Telegram bridge proof: reply with one sentence confirming this reached Leo HTTP.",
)
parser.add_argument("--output", default=DEFAULT_OUTPUT)
args = parser.parse_args()
proof = asyncio.run(run_check(args.url, args.message))
output_path = REPO_ROOT / args.output
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(proof, indent=2, sort_keys=True) + "\n")
print(json.dumps(proof, indent=2, sort_keys=True))
return 0 if proof["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -11,8 +11,8 @@ import logging
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
logger = logging.getLogger("tg.agent_config")
@ -43,6 +43,9 @@ class AgentConfig:
triage_model: str = "anthropic/claude-haiku-4.5"
max_tokens: int = 1024
max_response_per_user_per_hour: int = 30
http_chat_proxy_url: Optional[str] = None
respond_to_private_chats: bool = False
mention_aliases: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
"""Convert to dict for passing to build_system_prompt."""
@ -55,6 +58,9 @@ class AgentConfig:
"voice_summary": self.voice_summary,
"domain_expertise": self.domain_expertise,
"pentagon_agent_id": self.pentagon_agent_id,
"http_chat_proxy_url": self.http_chat_proxy_url,
"respond_to_private_chats": self.respond_to_private_chats,
"mention_aliases": self.mention_aliases,
}
@property
@ -100,6 +106,16 @@ def load_agent_config(config_path: str) -> AgentConfig:
if "learnings_file" not in raw:
errors.append("Missing required field: learnings_file")
proxy_url = raw.get("http_chat_proxy_url")
if proxy_url:
parsed_proxy = urlparse(proxy_url)
if parsed_proxy.scheme not in {"http", "https"} or not parsed_proxy.netloc:
errors.append("http_chat_proxy_url must be an absolute http(s) URL")
mention_aliases = raw.get("mention_aliases", [])
if mention_aliases and not isinstance(mention_aliases, list):
errors.append("mention_aliases must be a list")
if errors:
raise ValueError(
f"Agent config validation failed ({config_path}):\n"
@ -123,6 +139,9 @@ def load_agent_config(config_path: str) -> AgentConfig:
triage_model=raw.get("triage_model", "anthropic/claude-haiku-4.5"),
max_tokens=raw.get("max_tokens", 1024),
max_response_per_user_per_hour=raw.get("max_response_per_user_per_hour", 30),
http_chat_proxy_url=proxy_url,
respond_to_private_chats=bool(raw.get("respond_to_private_chats", False)),
mention_aliases=mention_aliases,
)

57
telegram/agents/leo.yaml Normal file
View file

@ -0,0 +1,57 @@
# Leo — Living IP x402 research agent
# This config makes Telegram a thin transport to Leo's hosted HTTP chat route.
# ─── Identity ────────────────────────────────────────────────────────────
name: Leo
handle: "@TeleoHumanBot"
x_handle: "@teLEOhuman"
mention_aliases:
- "@leo"
- "@teLEOhuman"
- "@TeleoHumanBot"
- "@teLEOhumanity"
bot_token_file: leo-telegram-bot-token
pentagon_agent_id: livingip-leo
domain: collective-intelligence
domain_expertise: >
collective intelligence, coordination systems, Living IP strategy, agent
markets, paid research, x402 service rails, and cross-domain synthesis
# ─── Hosted Leo Runtime ──────────────────────────────────────────────────
http_chat_proxy_url: "https://leo.livingip.xyz/api/agents/leo/chat"
respond_to_private_chats: true
# ─── KB Scope ────────────────────────────────────────────────────────────
kb_scope:
primary:
- domains/collective-intelligence
- domains/ai-alignment
- domains/space-development
- foundations
- core
# ─── Voice ───────────────────────────────────────────────────────────────
voice_summary: "Cross-domain strategist. Direct, synthesis-heavy, proof-aware."
voice_definition: |
## Register
You are Leo, TeleoHumanity's cross-domain strategy and collective
intelligence agent. Be direct and synthesis-heavy. Prefer concrete
mechanisms, coordination failures, and next actions over broad abstractions.
## x402 / Paid Research
When a user asks about paid services, research spend, or x402 capability,
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.
# ─── Learnings ───────────────────────────────────────────────────────────
learnings_file: agents/leo/learnings.md
# ─── Model ───────────────────────────────────────────────────────────────
response_model: anthropic/claude-opus-4-6
triage_model: anthropic/claude-haiku-4.5
max_tokens: 500
# ─── Rate Limits ─────────────────────────────────────────────────────────
max_response_per_user_per_hour: 30

View file

@ -50,6 +50,7 @@ from retrieval import orchestrate_retrieval
from market_data import get_token_price, format_price_context
from worktree_lock import main_worktree_lock
from x_client import search_tweets, fetch_from_url, check_research_rate_limit, record_research_usage, get_research_remaining
from http_chat_proxy import build_chat_proxy_payload, post_chat_proxy
# ─── Config ─────────────────────────────────────────────────────────────
@ -75,6 +76,15 @@ TRIAGE_MODEL = "anthropic/claude-haiku-4.5" # Haiku for batch triage
# KB scope — None means all domains (Rio default). Set from YAML config for other agents.
AGENT_KB_SCOPE: list[str] | None = None
AGENT_NAME = "Rio"
AGENT_HANDLE = "@FutAIrdBot"
AGENT_X_HANDLE = "@futaRdIO"
AGENT_DOMAIN_EXPERTISE = (
"futarchy, prediction markets, token governance, and the MetaDAO ecosystem"
)
AGENT_HTTP_CHAT_PROXY_URL: str | None = None
AGENT_RESPOND_TO_PRIVATE_CHATS = False
AGENT_MENTION_ALIASES = ["@teleo", "@FutAIrdBot"]
# Rate limits
MAX_RESPONSE_PER_USER_PER_HOUR = 30
@ -984,7 +994,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle messages that tag the bot — Rio responds with Opus."""
"""Handle messages that tag the bot."""
if not update.message or not update.message.text:
return
@ -999,6 +1009,82 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE):
logger.info("Tagged by @%s: %s", user.username if user else "unknown", text[:100])
if AGENT_HTTP_CHAT_PROXY_URL:
await msg.chat.send_action("typing")
payload = build_chat_proxy_payload(
message=text,
source="telegram",
agent=AGENT_NAME.lower(),
chat_id=msg.chat_id,
message_id=msg.message_id,
username=user.username if user else None,
)
try:
status, proxy_body, proxy_reply = await post_chat_proxy(
url=AGENT_HTTP_CHAT_PROXY_URL,
payload=payload,
)
except Exception as e:
logger.warning("%s HTTP chat proxy failed: %s", AGENT_NAME, e)
await msg.reply_text(
f"{AGENT_NAME}'s HTTP chat route is temporarily unavailable. "
"Try again after the service recovers.",
do_quote=True,
)
return
if not proxy_reply:
logger.warning("%s HTTP chat proxy returned no reply (status=%s)", AGENT_NAME, status)
await msg.reply_text(
f"{AGENT_NAME}'s HTTP chat route returned no usable reply. "
"The Telegram bridge is fail-closed.",
do_quote=True,
)
return
if len(proxy_reply) <= 4096:
await msg.reply_text(proxy_reply, do_quote=True)
else:
first = True
remaining = proxy_reply
while remaining:
chunk = remaining[:4096]
await msg.reply_text(chunk, quote=first)
first = False
remaining = remaining[4096:]
if user:
username = user.username or "anonymous"
key = (msg.chat_id, user.id)
unanswered_count[key] = 0
entry = {"user": text[:500], "bot": proxy_reply[:500], "username": username}
history = conversation_history.setdefault(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[user.id].append(time.time())
logger.info("%s proxied Telegram reply via HTTP chat route (status=%s)", AGENT_NAME, status)
_record_transcript(
msg,
proxy_reply,
is_bot=True,
rio_response=proxy_reply,
internal={
"agent": AGENT_NAME.lower(),
"http_chat_proxy": True,
"http_status": status,
"schema": proxy_body.get("schema") if isinstance(proxy_body, dict) else None,
"llm_ok": proxy_body.get("llmOk") if isinstance(proxy_body, dict) else None,
},
)
return
# ─── Audit: init timing and tool call tracking ──────────────────
response_start = time.monotonic()
tool_calls = []
@ -1913,9 +1999,9 @@ tags: [telegram, ownership-community]
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /start command."""
await update.message.reply_text(
"I'm Rio, the internet finance agent for TeleoHumanity's collective knowledge base. "
"Tag me with @teleo to ask about futarchy, prediction markets, token governance, "
"or anything in our domain. I'll ground my response in our KB's evidence."
f"I'm {AGENT_NAME}, a TeleoHumanity agent. "
f"Tag me with {AGENT_HANDLE} to ask about {AGENT_DOMAIN_EXPERTISE}. "
"I'll ground my response in the configured agent runtime."
)
@ -1937,10 +2023,21 @@ def _load_agent_config(config_path: str):
"""Load agent YAML config and set module-level variables."""
global BOT_TOKEN_FILE, RESPONSE_MODEL, TRIAGE_MODEL, AGENT_KB_SCOPE
global LEARNINGS_FILE, MAX_RESPONSE_PER_USER_PER_HOUR
global AGENT_NAME, AGENT_HANDLE, AGENT_X_HANDLE, AGENT_DOMAIN_EXPERTISE
global AGENT_HTTP_CHAT_PROXY_URL, AGENT_RESPOND_TO_PRIVATE_CHATS, AGENT_MENTION_ALIASES
with open(config_path) as f:
cfg = yaml.safe_load(f)
AGENT_NAME = cfg.get("name", AGENT_NAME)
AGENT_HANDLE = cfg.get("handle", AGENT_HANDLE)
AGENT_X_HANDLE = cfg.get("x_handle", AGENT_X_HANDLE)
AGENT_DOMAIN_EXPERTISE = cfg.get("domain_expertise", AGENT_DOMAIN_EXPERTISE)
AGENT_HTTP_CHAT_PROXY_URL = cfg.get("http_chat_proxy_url")
AGENT_RESPOND_TO_PRIVATE_CHATS = bool(cfg.get("respond_to_private_chats", False))
aliases = [AGENT_HANDLE, *cfg.get("mention_aliases", [])]
AGENT_MENTION_ALIASES = sorted({alias for alias in aliases if alias})
if cfg.get("bot_token_file"):
BOT_TOKEN_FILE = f"/opt/teleo-eval/secrets/{cfg['bot_token_file']}"
if cfg.get("response_model"):
@ -1959,6 +2056,17 @@ def _load_agent_config(config_path: str):
return cfg
def _mention_filter_regex(agent_cfg: dict | None) -> str:
"""Build the Telegram mention regex for the active agent."""
aliases = ["@teleo", "@FutAIrdBot"]
if agent_cfg:
aliases = [agent_cfg.get("handle", ""), *agent_cfg.get("mention_aliases", [])]
cleaned = sorted({alias for alias in aliases if alias})
if not cleaned:
cleaned = ["@teleo", "@FutAIrdBot"]
return "(?i)(" + "|".join(re.escape(alias) for alias in cleaned) + ")"
def main():
"""Start the bot."""
parser = argparse.ArgumentParser()
@ -2014,10 +2122,16 @@ def main():
# python-telegram-bot filters.Mention doesn't work for bot mentions in groups
# Use a regex filter for the bot username
app.add_handler(MessageHandler(
filters.TEXT & filters.Regex(r"(?i)(@teleo|@futairdbot)"),
filters.TEXT & filters.Regex(_mention_filter_regex(agent_cfg)),
handle_tagged,
))
if agent_cfg and AGENT_RESPOND_TO_PRIVATE_CHATS:
app.add_handler(MessageHandler(
filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND,
handle_tagged,
))
# Reply handler — replies to the bot's own messages continue the conversation
reply_to_bot_filter = filters.TEXT & filters.REPLY & ~filters.COMMAND
app.add_handler(MessageHandler(

View file

@ -0,0 +1,80 @@
"""HTTP chat proxy helpers for opt-in Telegram agent routing."""
from __future__ import annotations
from typing import Any
def build_chat_proxy_payload(
*,
message: str,
source: str,
agent: str,
chat_id: int | None = None,
message_id: int | None = None,
username: str | None = None,
) -> dict[str, Any]:
"""Build the no-secret payload sent from Telegram to an agent HTTP chat route."""
metadata = {
"source": source,
"agent": agent,
"chat_id": chat_id,
"message_id": message_id,
"username": username,
}
return {
"message": message,
"metadata": {k: v for k, v in metadata.items() if v is not None},
}
def extract_chat_proxy_reply(payload: dict[str, Any]) -> str | None:
"""Extract a reply from supported Living IP Leo chat response shapes."""
if not isinstance(payload, dict):
return None
direct_reply = payload.get("reply")
if isinstance(direct_reply, str) and direct_reply.strip():
return direct_reply.strip()
decision = payload.get("decision")
if isinstance(decision, dict):
decision_reply = decision.get("reply")
if isinstance(decision_reply, str) and decision_reply.strip():
return decision_reply.strip()
llm = payload.get("llm")
if isinstance(llm, dict):
llm_reply = llm.get("reply")
if isinstance(llm_reply, str) and llm_reply.strip():
return llm_reply.strip()
llm_decision = llm.get("decision")
if isinstance(llm_decision, dict):
llm_decision_reply = llm_decision.get("reply")
if isinstance(llm_decision_reply, str) and llm_decision_reply.strip():
return llm_decision_reply.strip()
return None
async def post_chat_proxy(
*,
url: str,
payload: dict[str, Any],
timeout_seconds: int = 30,
) -> tuple[int, dict[str, Any], str | None]:
"""POST to an agent HTTP chat route and return status, JSON body, and reply."""
import aiohttp
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout_seconds)) as session:
async with session.post(
url,
json=payload,
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"X-LivingIP-Source": "telegram-agent-proxy",
},
) as resp:
data = await resp.json(content_type=None)
return resp.status, data, extract_chat_proxy_reply(data)

View file

@ -0,0 +1,87 @@
"""Regression coverage for the Leo Telegram -> Living IP HTTP chat bridge."""
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
TELEGRAM_DIR = REPO_ROOT / "telegram"
sys.path.insert(0, str(TELEGRAM_DIR))
from agent_config import load_agent_config # noqa: E402
from http_chat_proxy import build_chat_proxy_payload, extract_chat_proxy_reply # noqa: E402
def test_leo_config_opts_into_http_chat_proxy_without_changing_default_agents():
leo = load_agent_config(str(TELEGRAM_DIR / "agents" / "leo.yaml"))
rio = load_agent_config(str(TELEGRAM_DIR / "agents" / "rio.yaml"))
assert leo.name == "Leo"
assert leo.http_chat_proxy_url == "https://leo.livingip.xyz/api/agents/leo/chat"
assert leo.respond_to_private_chats is True
assert "@teLEOhuman" in leo.mention_aliases
assert rio.http_chat_proxy_url is None
assert rio.respond_to_private_chats is False
def test_invalid_http_chat_proxy_url_fails_closed(tmp_path):
config = tmp_path / "bad.yaml"
config.write_text(
"""
name: Leo
handle: "@teLEOhuman"
bot_token_file: leo-telegram-bot-token
pentagon_agent_id: livingip-leo
domain: collective-intelligence
kb_scope:
primary: ["core"]
voice_summary: "test"
voice_definition: "test"
learnings_file: agents/leo/learnings.md
http_chat_proxy_url: "not-a-url"
""".strip()
)
with pytest.raises(ValueError, match="http_chat_proxy_url"):
load_agent_config(str(config))
def test_proxy_payload_contains_no_secret_material():
payload = build_chat_proxy_payload(
message="Can Leo use x402 paid research now?",
source="telegram",
agent="leo",
chat_id=123,
message_id=456,
username="tester",
)
assert payload == {
"message": "Can Leo use x402 paid research now?",
"metadata": {
"source": "telegram",
"agent": "leo",
"chat_id": 123,
"message_id": 456,
"username": "tester",
},
}
assert "token" not in str(payload).lower()
assert "secret" not in str(payload).lower()
@pytest.mark.parametrize(
("payload", "expected"),
[
({"reply": "public route reply"}, "public route reply"),
({"decision": {"reply": "analysis route reply"}}, "analysis route reply"),
({"llm": {"decision": {"reply": "nested decision reply"}}}, "nested decision reply"),
],
)
def test_extract_chat_proxy_reply_accepts_retained_leo_shapes(payload, expected):
assert extract_chat_proxy_reply(payload) == expected
def test_extract_chat_proxy_reply_fails_closed_on_missing_reply():
assert extract_chat_proxy_reply({"schema": "livingip.x402.leoChatResponse.v1"}) is None