Amber
  • Python 98.3%
  • HTML 1.3%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
andpuc23 20f1752c54 Set platform logo to the Amber image hosted on its eVault
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:37:18 +02:00
.w3ds Set platform logo to the Amber image hosted on its eVault 2026-09-30 19:37:18 +02:00
config Add real-time alerts: amber alert messages relevant posts as they arrive 2026-09-25 09:47:32 +02:00
linkedin_agent add "read full article" links follows, remove suggestion, add summaries 2026-09-27 22:22:46 +02:00
scripts Add real-time alerts: amber alert messages relevant posts as they arrive 2026-09-25 09:47:32 +02:00
tests add "read full article" links follows, remove suggestion, add summaries 2026-09-27 22:22:46 +02:00
.env.example Initial import of w3ds_amber 2026-09-24 21:47:24 +02:00
.gitignore fix titles parsing, add alert messages 2026-09-27 21:17:56 +02:00
pyproject.toml Initial import of w3ds_amber 2026-09-24 21:47:24 +02:00
README.md add "read full article" links follows, remove suggestion, add summaries 2026-09-27 22:22:46 +02:00
requirements.txt Initial import of w3ds_amber 2026-09-24 21:47:24 +02:00
test_mail add "read full article" links follows, remove suggestion, add summaries 2026-09-27 22:22:46 +02:00

LinkedIn Activity Monitoring Agent

A personal agent that tracks a predefined watchlist of LinkedIn accounts, posts, and topics, and turns new activity into a ranked digest of action points ("engage", "monitor", "reach_out").

No automated LinkedIn session, ever. This tool never logs into LinkedIn. Ingestion has two paths, both of which stay off linkedin.com:

  1. Email-notification ingestion (automatic) — LinkedIn already emails you when people/topics you follow are active. This tool polls a mailbox over IMAP and parses those emails.
  2. Manual paste (fallback) — a local web form for anything the notification stream doesn't cover.

One narrow exception: the digest-time relevance gate (relevance.enabled, on by default — see below) makes an unauthenticated HTTP GET of each item's linked article URL to decide whether it's on-topic. No login, no session, no cookies — and if LinkedIn returns a login wall the fetch is discarded and the gate falls back to the email excerpt. Set relevance.enabled: false to keep the whole pipeline off linkedin.com.

See linkedin_agent/future_automation/README.md for why a browser-automation path was deliberately not built, and what would have to be true before it ever should be.

One-time setup

  1. On linkedin.com: follow/enable notifications for the people and topics in your watchlist, subscribe to relevant hashtags, and turn on weekly digest emails.

  2. In your mail provider: create a filter that routes LinkedIn's notification emails into a dedicated folder/label. This keeps IMAP polling scoped to that folder instead of your whole inbox.

  3. Install dependencies:

    pip install -r requirements.txt
    
  4. Configure:

    cp .env.example .env            # fill in OPENAI_API_KEY and EMAIL_IMAP_*
    cp config/watchlist.yaml.example config/watchlist.yaml   # edit to your real watchlist
    

    (config/watchlist.yaml already exists with a starter watchlist — edit it directly if you'd rather.)

    For mailbox auth, .env.example documents two EMAIL_AUTH_MODE options:

    • app_password (default) — a Gmail App Password. Requires 2-Step Verification; unavailable on accounts with Advanced Protection or some Google Workspace policies.
    • oauth2 — for accounts where app passwords aren't offered. Requires pip install '.[oauth]' and a one-time amber oauth-setup run to mint a refresh token; see the comments in .env.example for the full Google Cloud OAuth client setup.

Usage

amber ingest-email      # poll the configured mailbox for new LinkedIn notification emails
amber serve             # open http://127.0.0.1:8420 to paste activity manually
amber digest            # relevance-filter + summarize pending activity into a ranked digest
amber feedback <id> acted|ignored|escalate
amber status            # watchlist size, pending/digested/skipped counts — read-only, no network

All commands can be run manually. Scheduling ingest-email and digest is safe because neither ever logs into LinkedIn — so both are wired to a scheduler.

Relevance gate

amber digest first runs each pending item through a topic-relevance filter (relevance: in config/watchlist.yaml). For every item it fetches the best full text it can get, in order:

  1. If the email itself carried a "read the full article" link off linkedin.com (external_url), fetch that directly — it's the real article, and unlike a LinkedIn post/pulse URL it usually isn't behind a login wall.
  2. Otherwise, fetch the LinkedIn post/pulse URL itself (source_url). If that page contains the same kind of "read the full article" link (common when the email didn't carry it but the live post does), follow that one hop too and prefer its text over the post's own.
  3. If neither yields an external article, use the post's own fetched text.

then asks the model whether the content substantively concerns one of your topics. Items judged off-topic are set to digest_status='skipped' (with a one-line reason stored in skip_reason) and left out of the digest; they are not re-evaluated on later runs, but stay in data/app.db so you can audit or reverse a decision. The fetch is fail-open: a network error, a login wall, or a judge error keeps the item rather than dropping it. amber status shows the running skipped count. Turn the whole stage off with relevance.enabled: false.

Scheduled email ingestion

scripts/cron_ingest_email.sh runs amber ingest-email from the project's venv and appends output to data/logs/ingest_email.log. Install it as an hourly cron job:

crontab -e
# then add:
0 * * * * /path/to/w3ds_amber/scripts/cron_ingest_email.sh

Scheduled digest

scripts/cron_digest.sh runs amber digest from the project's venv and appends output to data/logs/digest.log. It publishes through whatever output.adapter is set in config/watchlist.yaml — set it to email and fill in output.email.to (recipient) and the SMTP_* vars in .env to have the digest land in an inbox instead of data/digests/. If there's no pending activity, amber digest is a no-op — no email gets sent. Install as a daily cron job, e.g. 10am:

crontab -e
# then add:
0 10 * * * /path/to/w3ds_amber/scripts/cron_digest.sh

Both are per-machine crontab entries, not part of the repo config. serve and feedback remain manual-trigger only.

Real-time alerts

amber alert handles pending items one by one as soon as they are ingested: relevance gate (off-topic items become skipped), then the model picks an engage level (HIGH / MEDIUM / LOW) and summarizes the fetched article, then the result is sent as a W3DS message to every w3ds.receivers eName:

Engage level: HIGH
[Jane Doe](https://www.linkedin.com/in/jane/) posted
[Post title](https://www.linkedin.com/posts/...)
Topic: <matched watched topic>
Summary: <summary of the fetched full article, ~10% of its length>

The summary line reuses the same unauthenticated article fetch the relevance gate already makes for that item (see below) — no extra request to linkedin.com beyond what that fetch already needed. That fetch prefers an external "read the full article" link, whether it was captured from the email directly or discovered by reading the LinkedIn post/pulse page itself, over the LinkedIn URL's own text — see "Relevance gate" below for the full order. The summary only appears when a fetch succeeded; if the relevance gate is disabled or every fetch attempt failed (login wall, network error), the alert has no summary line — it is never derived from the short email excerpt.

A drafted reply is still computed internally (linkedin_agent/alerts/suggester.py, AlertSuggestion.suggestion) for reuse by a future feature, but is no longer part of the alert message — the summary replaced it there.

Every attempt (sent or failed) is stored in the alerts table of data/app.db (exact message, level, per-receiver outcome), and logged to data/logs/ingest_email.log when run by cron. Alerted items get digest_status='alerted' and no longer appear in the daily digest; a failed send leaves the item pending so the next run retries it. alerts.max_per_run (default 10) caps a run so an old backlog can't flood the chat. The account name is a link only for watchlist accounts; topic and post matches have no profile URL.

scripts/cron_ingest_email.sh runs alert after ingest-email, so the hourly ingest cron job is what makes alerts near-real-time.

Publishing a digest elsewhere

amber digest renders the digest through the adapter named by output.adapter in the watchlist config:

  • file (default) — writes a markdown file under data/digests/.
  • email — renders an HTML + plaintext message and sends it over SMTP to output.email.to. SMTP credentials come from .env (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, optional SMTP_STARTTLS / SMTP_FROM); use an app-specific password. See config/watchlist.yaml.example for the config block and .env.example for the credentials.
  • cloud — still a stub (output/cloud_adapter.py).

New delivery channels (Telegram, Slack, …) just need another OutputAdapter subclass wired into output/__init__.py.

Tests

pytest

Unit tests only — no network calls, no real LinkedIn/email/OpenAI access required.