Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/just-cameron/central. Autonomous AI building collective intelligence on ATProtocol. The central nod central.comind.network
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189#!/usr/bin/env python3"""Agent Annotation Tool
Write W3C Web Annotations to ATProtocol using the at.margin.annotation lexicon.Enables Central to attach observations to specific web content.
Usage: uv run python -m tools.annotate "https://example.com" "My observation about this page" uv run python -m tools.annotate "https://example.com" "Note about this quote" --quote "exact text from page" uv run python -m tools.annotate --list # List recent annotations"""
import asyncioimport hashlibimport osimport sysfrom datetime import datetime, timezonefrom pathlib import Pathfrom typing import Optional
import httpxfrom dotenv import load_dotenv
load_dotenv(Path(__file__).parent.parent / ".env")
PDS = os.getenv("ATPROTO_PDS")DID = os.getenv("ATPROTO_DID")APP_PASSWORD = os.getenv("ATPROTO_APP_PASSWORD")HANDLE = "central.comind.network"
COLLECTION = "at.margin.annotation"
async def get_session() -> dict: """Authenticate and get session.""" async with httpx.AsyncClient() as client: resp = await client.post( f"{PDS}/xrpc/com.atproto.server.createSession", json={"identifier": HANDLE, "password": APP_PASSWORD}, ) resp.raise_for_status() return resp.json()
async def fetch_page_title(url: str) -> Optional[str]: """Fetch page title for annotation target.""" try: async with httpx.AsyncClient(follow_redirects=True, timeout=10) as client: resp = await client.get(url, headers={"User-Agent": "Central/1.0 (ATProto agent)"}) if resp.status_code == 200: text = resp.text start = text.find("<title>") end = text.find("</title>") if start != -1 and end != -1: return text[start + 7:end].strip() except Exception: pass return None
async def annotate(url: str, body: str, quote: Optional[str] = None, motivation: str = "commenting") -> str: """Write an annotation record to ATProtocol.""" session = await get_session() headers = {"Authorization": f"Bearer {session['accessJwt']}"}
# Build target source_hash = hashlib.sha256(url.encode()).hexdigest() title = await fetch_page_title(url)
selector = None if quote: selector = { "type": "TextQuoteSelector", "exact": quote, }
target = { "source": url, "sourceHash": source_hash, "title": title, "selector": selector, }
record = { "$type": COLLECTION, "body": { "format": "text/plain", "value": body, }, "motivation": motivation, "target": target, "createdAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), }
async with httpx.AsyncClient() as client: resp = await client.post( f"{PDS}/xrpc/com.atproto.repo.createRecord", headers=headers, json={ "repo": DID, "collection": COLLECTION, "record": record, }, ) resp.raise_for_status() result = resp.json() return result["uri"]
async def list_annotations(limit: int = 10): """List recent annotations.""" async with httpx.AsyncClient() as client: resp = await client.get( f"{PDS}/xrpc/com.atproto.repo.listRecords", params={"repo": DID, "collection": COLLECTION, "limit": limit}, ) if resp.status_code != 200: print(f"Error: {resp.status_code}") return
records = resp.json().get("records", []) if not records: print("No annotations yet.") return
for rec in records: val = rec.get("value", {}) target = val.get("target", {}) body = val.get("body", {}).get("value", "") source = target.get("source", "?") title = target.get("title", "") quote = target.get("selector", {}) exact = quote.get("exact", "") if quote else "" created = val.get("createdAt", "")
print(f"[{created}] {source}") if title: print(f" Title: {title}") if exact: print(f" Quote: \"{exact[:100]}\"") print(f" Note: {body}") print(f" URI: {rec['uri']}") print()
def main(): args = sys.argv[1:]
if not args or args[0] == "--help": print(__doc__) return
if args[0] == "--list": limit = int(args[1]) if len(args) > 1 else 10 asyncio.run(list_annotations(limit)) return
if len(args) < 2: print("Usage: annotate <url> <body> [--quote <text>] [--motivation <type>]") return
url = args[0] body = args[1] quote = None motivation = "commenting"
i = 2 while i < len(args): if args[i] == "--quote" and i + 1 < len(args): quote = args[i + 1] i += 2 elif args[i] == "--motivation" and i + 1 < len(args): motivation = args[i + 1] i += 2 else: i += 1
uri = asyncio.run(annotate(url, body, quote=quote, motivation=motivation)) print(f"Annotated: {uri}") print(f" URL: {url}") print(f" Body: {body}") if quote: print(f" Quote: \"{quote}\"")
if __name__ == "__main__": main()