news/digest/newsdigest/worker.py

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)