Add Leo Telegram x402 bridge
This commit is contained in:
parent
377924dabe
commit
b769eb68b6
11 changed files with 692 additions and 6 deletions
|
|
@ -13,6 +13,7 @@ fi
|
||||||
|
|
||||||
DEPLOY_CHECKOUT="/opt/teleo-eval/workspaces/deploy-infra"
|
DEPLOY_CHECKOUT="/opt/teleo-eval/workspaces/deploy-infra"
|
||||||
PIPELINE_DIR="/opt/teleo-eval/pipeline"
|
PIPELINE_DIR="/opt/teleo-eval/pipeline"
|
||||||
|
TELEGRAM_DIR="/opt/teleo-eval/telegram"
|
||||||
DIAGNOSTICS_DIR="/opt/teleo-eval/diagnostics"
|
DIAGNOSTICS_DIR="/opt/teleo-eval/diagnostics"
|
||||||
AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state"
|
AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state"
|
||||||
STAMP_FILE="/opt/teleo-eval/.last-deploy-sha"
|
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
|
done
|
||||||
|
|
||||||
rsync "${RSYNC_OPTS[@]}" telegram/ "$PIPELINE_DIR/telegram/"
|
rsync "${RSYNC_OPTS[@]}" telegram/ "$PIPELINE_DIR/telegram/"
|
||||||
|
rsync "${RSYNC_OPTS[@]}" telegram/ "$TELEGRAM_DIR/"
|
||||||
rsync "${RSYNC_OPTS[@]}" diagnostics/ "$DIAGNOSTICS_DIR/"
|
rsync "${RSYNC_OPTS[@]}" diagnostics/ "$DIAGNOSTICS_DIR/"
|
||||||
rsync "${RSYNC_OPTS[@]}" agent-state/ "$AGENT_STATE_DIR/"
|
rsync "${RSYNC_OPTS[@]}" agent-state/ "$AGENT_STATE_DIR/"
|
||||||
rsync "${RSYNC_OPTS[@]}" tests/ "$PIPELINE_DIR/tests/"
|
rsync "${RSYNC_OPTS[@]}" tests/ "$PIPELINE_DIR/tests/"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ set -euo pipefail
|
||||||
|
|
||||||
VPS_HOST="teleo@77.42.65.182"
|
VPS_HOST="teleo@77.42.65.182"
|
||||||
VPS_PIPELINE="/opt/teleo-eval/pipeline"
|
VPS_PIPELINE="/opt/teleo-eval/pipeline"
|
||||||
|
VPS_TELEGRAM="/opt/teleo-eval/telegram"
|
||||||
VPS_DIAGNOSTICS="/opt/teleo-eval/diagnostics"
|
VPS_DIAGNOSTICS="/opt/teleo-eval/diagnostics"
|
||||||
VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
|
VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
@ -74,6 +75,7 @@ echo ""
|
||||||
|
|
||||||
echo "=== Telegram bot ==="
|
echo "=== Telegram bot ==="
|
||||||
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_PIPELINE/telegram/"
|
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_PIPELINE/telegram/"
|
||||||
|
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/telegram/" "$VPS_HOST:$VPS_TELEGRAM/"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "=== Tests ==="
|
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 os
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
logger = logging.getLogger("tg.agent_config")
|
logger = logging.getLogger("tg.agent_config")
|
||||||
|
|
||||||
|
|
@ -43,6 +43,9 @@ class AgentConfig:
|
||||||
triage_model: str = "anthropic/claude-haiku-4.5"
|
triage_model: str = "anthropic/claude-haiku-4.5"
|
||||||
max_tokens: int = 1024
|
max_tokens: int = 1024
|
||||||
max_response_per_user_per_hour: int = 30
|
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:
|
def to_dict(self) -> dict:
|
||||||
"""Convert to dict for passing to build_system_prompt."""
|
"""Convert to dict for passing to build_system_prompt."""
|
||||||
|
|
@ -55,6 +58,9 @@ class AgentConfig:
|
||||||
"voice_summary": self.voice_summary,
|
"voice_summary": self.voice_summary,
|
||||||
"domain_expertise": self.domain_expertise,
|
"domain_expertise": self.domain_expertise,
|
||||||
"pentagon_agent_id": self.pentagon_agent_id,
|
"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
|
@property
|
||||||
|
|
@ -100,6 +106,16 @@ def load_agent_config(config_path: str) -> AgentConfig:
|
||||||
if "learnings_file" not in raw:
|
if "learnings_file" not in raw:
|
||||||
errors.append("Missing required field: learnings_file")
|
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:
|
if errors:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Agent config validation failed ({config_path}):\n"
|
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"),
|
triage_model=raw.get("triage_model", "anthropic/claude-haiku-4.5"),
|
||||||
max_tokens=raw.get("max_tokens", 1024),
|
max_tokens=raw.get("max_tokens", 1024),
|
||||||
max_response_per_user_per_hour=raw.get("max_response_per_user_per_hour", 30),
|
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 market_data import get_token_price, format_price_context
|
||||||
from worktree_lock import main_worktree_lock
|
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 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 ─────────────────────────────────────────────────────────────
|
# ─── 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.
|
# KB scope — None means all domains (Rio default). Set from YAML config for other agents.
|
||||||
AGENT_KB_SCOPE: list[str] | None = None
|
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
|
# Rate limits
|
||||||
MAX_RESPONSE_PER_USER_PER_HOUR = 30
|
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):
|
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:
|
if not update.message or not update.message.text:
|
||||||
return
|
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])
|
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 ──────────────────
|
# ─── Audit: init timing and tool call tracking ──────────────────
|
||||||
response_start = time.monotonic()
|
response_start = time.monotonic()
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
|
|
@ -1913,9 +1999,9 @@ tags: [telegram, ownership-community]
|
||||||
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Handle /start command."""
|
"""Handle /start command."""
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"I'm Rio, the internet finance agent for TeleoHumanity's collective knowledge base. "
|
f"I'm {AGENT_NAME}, a TeleoHumanity agent. "
|
||||||
"Tag me with @teleo to ask about futarchy, prediction markets, token governance, "
|
f"Tag me with {AGENT_HANDLE} to ask about {AGENT_DOMAIN_EXPERTISE}. "
|
||||||
"or anything in our domain. I'll ground my response in our KB's evidence."
|
"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."""
|
"""Load agent YAML config and set module-level variables."""
|
||||||
global BOT_TOKEN_FILE, RESPONSE_MODEL, TRIAGE_MODEL, AGENT_KB_SCOPE
|
global BOT_TOKEN_FILE, RESPONSE_MODEL, TRIAGE_MODEL, AGENT_KB_SCOPE
|
||||||
global LEARNINGS_FILE, MAX_RESPONSE_PER_USER_PER_HOUR
|
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:
|
with open(config_path) as f:
|
||||||
cfg = yaml.safe_load(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"):
|
if cfg.get("bot_token_file"):
|
||||||
BOT_TOKEN_FILE = f"/opt/teleo-eval/secrets/{cfg['bot_token_file']}"
|
BOT_TOKEN_FILE = f"/opt/teleo-eval/secrets/{cfg['bot_token_file']}"
|
||||||
if cfg.get("response_model"):
|
if cfg.get("response_model"):
|
||||||
|
|
@ -1959,6 +2056,17 @@ def _load_agent_config(config_path: str):
|
||||||
return cfg
|
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():
|
def main():
|
||||||
"""Start the bot."""
|
"""Start the bot."""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
@ -2014,10 +2122,16 @@ def main():
|
||||||
# python-telegram-bot filters.Mention doesn't work for bot mentions in groups
|
# python-telegram-bot filters.Mention doesn't work for bot mentions in groups
|
||||||
# Use a regex filter for the bot username
|
# Use a regex filter for the bot username
|
||||||
app.add_handler(MessageHandler(
|
app.add_handler(MessageHandler(
|
||||||
filters.TEXT & filters.Regex(r"(?i)(@teleo|@futairdbot)"),
|
filters.TEXT & filters.Regex(_mention_filter_regex(agent_cfg)),
|
||||||
handle_tagged,
|
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 handler — replies to the bot's own messages continue the conversation
|
||||||
reply_to_bot_filter = filters.TEXT & filters.REPLY & ~filters.COMMAND
|
reply_to_bot_filter = filters.TEXT & filters.REPLY & ~filters.COMMAND
|
||||||
app.add_handler(MessageHandler(
|
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