Add Leo Telegram x402 bridge
This commit is contained in:
parent
71ea7a625c
commit
2e7d4e7450
11 changed files with 692 additions and 6 deletions
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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 ==="
|
||||
|
|
|
|||
21
docs/reports/telegram-leo-x402-bridge-proof.json
Normal file
21
docs/reports/telegram-leo-x402-bridge-proof.json
Normal 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"
|
||||
}
|
||||
83
docs/telegram-leo-x402-bridge-pr-packet.md
Normal file
83
docs/telegram-leo-x402-bridge-pr-packet.md
Normal 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.
|
||||
133
docs/telegram-leo-x402-priority-spec.md
Normal file
133
docs/telegram-leo-x402-priority-spec.md
Normal 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.
|
||||
88
scripts/check_telegram_leo_x402_bridge.py
Normal file
88
scripts/check_telegram_leo_x402_bridge.py
Normal 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())
|
||||
|
|
@ -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
57
telegram/agents/leo.yaml
Normal 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
|
||||
124
telegram/bot.py
124
telegram/bot.py
|
|
@ -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(
|
||||
|
|
|
|||
80
telegram/http_chat_proxy.py
Normal file
80
telegram/http_chat_proxy.py
Normal 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)
|
||||
87
tests/test_telegram_leo_x402_bridge.py
Normal file
87
tests/test_telegram_leo_x402_bridge.py
Normal 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
|
||||
Loading…
Reference in a new issue