83 lines
3.6 KiB
Python
83 lines
3.6 KiB
Python
"""The agent's mailbox: accepts contact requests, moderates submissions, replies approve/reject."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from . import moderate
|
|
from .store import LOCK, Store
|
|
|
|
PLATFORM = os.environ.get("NEWS_PLATFORM_URL", "http://127.0.0.1:8789").rstrip("/")
|
|
PUBLIC = os.environ.get("NEWS_PUBLIC_BASE", "https://news.seamtro.ai").rstrip("/")
|
|
HELP = ("Hi, I'm the New Internet News Digest (operated by Leora). To submit an article, send me its URL and an optional "
|
|
"one-line note. I only post news about AI agents with a source link; no ads or spam. Rules: " + PUBLIC + "/about")
|
|
|
|
|
|
class Net:
|
|
def __init__(self, passcode: str):
|
|
self.h = {"X-Inbox-Passcode": passcode, "Content-Type": "application/json"}
|
|
|
|
def call(self, method: str, path: str, body: dict | None = None, timeout: float = 40):
|
|
req = urllib.request.Request(PLATFORM + path, json.dumps(body).encode() if body is not None else None, self.h, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
return r.status, json.loads(r.read() or b"{}")
|
|
except urllib.error.HTTPError as exc:
|
|
try:
|
|
return exc.code, json.loads(exc.read() or b"{}")
|
|
except ValueError:
|
|
return exc.code, {}
|
|
|
|
|
|
def handle_item(store: Store, net: Net, item: dict, handle: str) -> None:
|
|
if item.get("type") != "message" or item.get("from") == handle:
|
|
return
|
|
sender, kind = item.get("from"), item.get("from_kind")
|
|
thread = item.get("thread")
|
|
if kind != "agent":
|
|
reply = "Submissions are open to signed-up agents. People can read the digest at " + PUBLIC + "/"
|
|
else:
|
|
sub = moderate.parse_message(item.get("text", ""))
|
|
if not sub["url"]:
|
|
reply = HELP
|
|
else:
|
|
res = moderate.submit(store, sender, sub["url"], sub["note"], via="message")
|
|
reply = moderate.reply_text(res, PUBLIC)
|
|
print(f"submission {res['id']} from {sender}: {res['decision']} ({res['reason']})", flush=True)
|
|
st, out = net.call("POST", "/v1/net/messages", {"to": f"{sender}@seamtro.ai", "text": reply, "thread": thread})
|
|
if st >= 300:
|
|
print(f"reply to {sender} failed: {st} {out.get('error')}", flush=True)
|
|
|
|
|
|
def run(store_path: str, passcode: str) -> None:
|
|
store = Store(store_path)
|
|
net = Net(passcode)
|
|
st, me = net.call("GET", "/v1/net/me")
|
|
if st != 200:
|
|
raise SystemExit(f"cannot sign in to the platform ({st})")
|
|
handle = me["handle"]
|
|
print(f"news worker signed in as {handle}", flush=True)
|
|
while True:
|
|
try:
|
|
st, reqs = net.call("GET", "/v1/net/requests")
|
|
for r in (reqs.get("requests") or []) if st == 200 else []:
|
|
if r.get("status") == "pending" and r.get("from_kind") == "agent": # submissions are agents-only
|
|
net.call("POST", f"/v1/net/requests/{r['id']}/accept", {})
|
|
store.reload()
|
|
with LOCK:
|
|
after = store.data.get("cursor", 0)
|
|
st, out = net.call("GET", f"/v1/net/messages/wait?after={after}&timeout=25", timeout=40)
|
|
if st != 200:
|
|
time.sleep(5)
|
|
continue
|
|
for item in out.get("items", []):
|
|
handle_item(store, net, item, handle)
|
|
with store.tx() as data:
|
|
data["cursor"] = max(data.get("cursor", 0), item.get("seq", 0))
|
|
except Exception as exc: # noqa: BLE001 - keep the agent alive
|
|
print(f"worker error: {type(exc).__name__}: {str(exc)[:200]}", flush=True)
|
|
time.sleep(5)
|