system-prompts-and-models-o.../personal-brand-engine/agents/linkedin/engagement.py
VoXc2 4bb2442313
Add Personal Brand Engine - 7 AI Agents Automation System
Complete AI-powered personal brand automation for Sami Assiri.\n\n7 agents: LinkedIn, Email, Social Media, WhatsApp, CV Optimizer, Content Strategist, Opportunity Scout.\nInfra: FastAPI + APScheduler + Docker + Ollama/Groq LLM + GitHub Pages landing page.\n83 files, ~10K lines. Cost: $0-5/month.
2026-03-30 11:45:48 +03:00

211 lines
6.1 KiB
Python

"""Engage with the LinkedIn feed -- like and comment on relevant posts."""
from __future__ import annotations
import logging
import random
from pathlib import Path
import yaml
logger = logging.getLogger(__name__)
_TEMPLATES_DIR = Path(__file__).resolve().parent / "prompts"
# Topics Sami cares about (used for relevance filtering)
TARGET_KEYWORDS = [
"airport security",
"aviation",
"smiths detection",
"metco",
"x-ray",
"screening",
"baggage",
"checkpoint",
"ct scanner",
"trace detection",
"eds",
"engineering",
"field service",
"epc",
"gaca",
"icao",
"saudi arabia",
"spe",
"petroleum",
"oil and gas",
"أمن المطارات",
"هندسة",
"الطيران",
]
COMMENT_SYSTEM_PROMPT = """\
You are writing a LinkedIn comment on behalf of Sami Mohammed Assiri, a Field \
Services Engineer at METCO (Smiths Detection) specialising in airport security \
technology.
Guidelines:
- Be genuine and insightful -- add real value, not generic praise.
- Reference a specific point from the post when possible.
- Keep it between 1 and 3 sentences.
- Maintain a professional yet warm tone.
- Do NOT be sycophantic ("Great post!", "Love this!", "Amazing insight!").
- If the post is in Arabic, comment in Arabic. Otherwise, use English.
- Never self-promote or include links.
"""
def _load_comment_templates() -> dict:
"""Load comment_templates.yaml."""
path = _TEMPLATES_DIR / "comment_templates.yaml"
if not path.exists():
return {}
with open(path, "r", encoding="utf-8") as fh:
return yaml.safe_load(fh) or {}
def _is_relevant(post_text: str) -> bool:
"""Rough keyword check to decide if a post is worth engaging with."""
lower = post_text.lower()
return any(kw in lower for kw in TARGET_KEYWORDS)
def _extract_post_text(post: dict) -> str:
"""Safely pull the textual content from a linkedin-api post dict."""
try:
commentary = (
post.get("commentary", "")
or post.get("specificContent", {})
.get("com.linkedin.ugc.ShareContent", {})
.get("shareCommentary", {})
.get("text", "")
)
return commentary or ""
except (AttributeError, TypeError):
return ""
def _extract_post_urn(post: dict) -> str | None:
"""Extract the post URN (entity ID) from a feed post dict."""
return post.get("dashEntityUrn") or post.get("entityUrn") or post.get("urn")
async def _generate_comment(llm_client, post_text: str, brand_profile: dict) -> str:
"""Use the LLM to craft a thoughtful comment for the given post."""
templates = _load_comment_templates()
# Provide a few example styles to guide the LLM
example_block = ""
categories = list(templates.values()) if templates else []
if categories:
flat = [t for cat in categories for t in (cat if isinstance(cat, list) else [])]
if flat:
samples = random.sample(flat, min(2, len(flat)))
example_block = (
"\nExample comment styles (do NOT copy verbatim):\n"
+ "\n".join(f"- {s}" for s in samples)
+ "\n"
)
user_prompt = (
f"Original LinkedIn post:\n\"\"\"\n{post_text[:1500]}\n\"\"\"\n\n"
f"{example_block}\n"
f"Write a comment as Sami. Return ONLY the comment text."
)
response = await llm_client.generate(
prompt=user_prompt,
system_prompt=COMMENT_SYSTEM_PROMPT,
temperature=0.75,
max_tokens=300,
)
return response.text.strip().strip('"')
async def engage_with_feed(
linkedin_api,
llm_client,
brand_profile: dict,
max_likes: int = 15,
max_comments: int = 5,
) -> dict:
"""Like and comment on recent relevant posts in Sami's LinkedIn feed.
Parameters
----------
linkedin_api:
Authenticated ``linkedin_api.Linkedin`` instance.
llm_client:
LLM client for generating comments.
brand_profile:
Parsed brand profile dict.
max_likes:
Maximum number of posts to like in this round.
max_comments:
Maximum number of posts to comment on in this round.
Returns
-------
dict
Summary of actions taken (likes, comments, errors).
"""
liked = 0
commented = 0
errors: list[str] = []
try:
feed = linkedin_api.get_feed_posts(limit=50)
except Exception as exc:
logger.error("Failed to fetch feed: %s", exc)
return {"liked": 0, "commented": 0, "errors": [str(exc)]}
if not feed:
logger.info("Feed returned no posts.")
return {"liked": 0, "commented": 0, "errors": []}
# Shuffle to avoid always engaging with the same people
random.shuffle(feed)
for post in feed:
if liked >= max_likes and commented >= max_comments:
break
post_text = _extract_post_text(post)
post_urn = _extract_post_urn(post)
if not post_urn:
continue
# --- Like ---
if liked < max_likes:
try:
linkedin_api.like(post_urn)
liked += 1
logger.debug("Liked post %s", post_urn)
except Exception as exc:
errors.append(f"Like failed ({post_urn}): {exc}")
logger.warning("Failed to like %s: %s", post_urn, exc)
# --- Comment (only on relevant posts) ---
if commented < max_comments and post_text and _is_relevant(post_text):
try:
comment_text = await _generate_comment(
llm_client, post_text, brand_profile
)
linkedin_api.comment(post_urn, comment_text)
commented += 1
logger.info(
"Commented on %s: %s", post_urn, comment_text[:80]
)
except Exception as exc:
errors.append(f"Comment failed ({post_urn}): {exc}")
logger.warning("Failed to comment on %s: %s", post_urn, exc)
summary = {
"liked": liked,
"commented": commented,
"errors": errors[:10], # cap stored errors
}
logger.info("Engagement round complete: %s", summary)
return summary