forked from retoor/devplacepy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
239 lines
9.5 KiB
Python
239 lines
9.5 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import logging
|
|
import random
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BotAgentMixin:
|
|
def _build_menu(self, page_state: dict) -> list[dict]:
|
|
page = page_state["page"]
|
|
menu: list[dict] = []
|
|
|
|
def add(action: str, desc: str) -> None:
|
|
menu.append({"action": action, "desc": desc})
|
|
|
|
if page == "post":
|
|
if not page_state["own_post"] or page_state["mentioned"]:
|
|
add("comment", "write a comment on this post")
|
|
add("reply", "reply to an existing comment in the thread")
|
|
add("vote", "upvote this post")
|
|
add("vote_comment", "upvote a good comment in the thread")
|
|
add("vote_poll", "answer the attached poll if there is one")
|
|
add("react", "add an emoji reaction to this post")
|
|
elif page == "news_detail":
|
|
add("comment", "write a comment reacting to this news article")
|
|
elif page == "project_detail":
|
|
add("vote", "star this project")
|
|
add("react", "add an emoji reaction")
|
|
add("comment", "write a comment on this project")
|
|
elif page == "gist_detail":
|
|
add("comment", "write a comment on this code snippet")
|
|
add("vote", "star this gist")
|
|
add("react", "add an emoji reaction")
|
|
elif page == "profile":
|
|
if page_state.get("own_profile"):
|
|
if not self.state.profile_filled:
|
|
add("update_profile", "fill in your own profile bio and links")
|
|
add("open_post", "open one of your posts to read it")
|
|
else:
|
|
if not page_state["follows"]:
|
|
add("follow", "follow this developer")
|
|
if page_state["can_message"]:
|
|
add("message", "send this developer a direct message")
|
|
add("open_post", "open one of their posts to read it")
|
|
elif page == "projects_list":
|
|
add("vote", "star a project in the list")
|
|
if page_state["can_project"]:
|
|
add("create_project", "create a new project of your own")
|
|
add("open_project", "open a project to read it")
|
|
elif page == "gists_list":
|
|
add("vote", "star a gist in the list")
|
|
if not page_state["gists"]:
|
|
add("create_gist", "publish a new code snippet of your own")
|
|
add("open_gist", "open a gist to read it")
|
|
elif page == "news_list":
|
|
add("open_news", "open a news article to read it")
|
|
elif page == "notifications":
|
|
add("check_notifications", "read and act on your notifications")
|
|
else:
|
|
add("open_post", "open a post from the feed to read it")
|
|
add("vote", "upvote a post in the feed")
|
|
add("react", "react to a post in the feed")
|
|
add("vote_poll", "answer a poll in the feed")
|
|
if page_state["can_post"] and not page_state["did_post"]:
|
|
add("create_post", "write a new post reacting to a tech news article")
|
|
if not page_state["gists"]:
|
|
add("create_gist", "publish a new code snippet of your own")
|
|
add("open_profile", "open another developer's profile")
|
|
add("search", "search the platform for a topic you care about")
|
|
add("browse", "browse a category or section")
|
|
if page != "notifications" and page_state.get("unread_notifications"):
|
|
add(
|
|
"check_notifications",
|
|
"open your notifications and reply to anyone who mentioned or replied to you",
|
|
)
|
|
add("navigate", "move to another section: feed, projects, gists, or news")
|
|
return menu
|
|
|
|
def _record_history(self, action: str, rationale: str) -> None:
|
|
note = action.replace("_", " ")
|
|
if rationale:
|
|
note = f"{note} ({rationale[:60]})"
|
|
self._session_history.append(note)
|
|
if len(self._session_history) > 20:
|
|
self._session_history = self._session_history[-20:]
|
|
|
|
async def _vote_for_page(self, page: str) -> bool:
|
|
if page in ("project_detail", "projects_list"):
|
|
return await self._vote_on_project()
|
|
if page in ("gist_detail", "gists_list"):
|
|
return await self._vote_on_gist()
|
|
return await self._vote_on_feed()
|
|
|
|
async def _navigate_to(self, target: str) -> bool:
|
|
routes = {
|
|
"feed": "/feed",
|
|
"projects": "/projects",
|
|
"gists": "/gists",
|
|
"news": "/news",
|
|
"notifications": "/notifications",
|
|
}
|
|
path = routes.get((target or "").lower())
|
|
if path:
|
|
await self.b.goto(f"{self.base_url}{path}")
|
|
return True
|
|
return await self._random_nav_click()
|
|
|
|
async def _dispatch_action(self, action: str, target: str, page_state: dict) -> bool:
|
|
if action == "navigate":
|
|
return await self._navigate_to(target)
|
|
if action == "vote":
|
|
return await self._vote_for_page(page_state["page"])
|
|
handlers = {
|
|
"comment": self._comment_on_post,
|
|
"reply": self._reply_to_random_comment,
|
|
"vote_comment": self._vote_on_comments,
|
|
"vote_poll": self._vote_on_poll,
|
|
"react": self._react_to_object,
|
|
"create_post": self._create_post,
|
|
"create_gist": self._create_gist,
|
|
"create_project": self._click_create_project,
|
|
"update_profile": self._update_profile,
|
|
"follow": self._follow_user,
|
|
"message": self._send_message,
|
|
"open_post": self._click_post_link,
|
|
"open_gist": self._click_gist_link,
|
|
"open_profile": self._click_profile_link,
|
|
"open_project": lambda: self._open_content("project"),
|
|
"open_news": lambda: self._open_content("news"),
|
|
"check_notifications": self._check_notifications,
|
|
"search": lambda: self._search("feed"),
|
|
"browse": self._browse_section,
|
|
}
|
|
handler = handlers.get(action)
|
|
if handler is None:
|
|
return False
|
|
try:
|
|
result = await handler()
|
|
except Exception as e:
|
|
logger.debug("dispatch %s failed: %s", action, e)
|
|
return False
|
|
return True if result is None else bool(result)
|
|
|
|
async def _run_plan(
|
|
self, plan: list[dict], page_state: dict
|
|
) -> tuple[int, bool, int, int, int]:
|
|
b = self.b
|
|
actions = 0
|
|
did_post = False
|
|
projs = 0
|
|
follows = 0
|
|
gists = 0
|
|
if not plan:
|
|
await self._navigate_to("feed")
|
|
return 0, did_post, projs, follows, gists
|
|
for entry in plan:
|
|
action = entry["action"]
|
|
if action == "create_post" and (did_post or page_state["did_post"]):
|
|
continue
|
|
if action == "create_gist" and (gists or page_state["gists"]):
|
|
continue
|
|
if action == "create_project" and projs:
|
|
continue
|
|
if await self._dispatch_action(action, entry.get("target", ""), page_state):
|
|
actions += 1
|
|
self._record_history(action, entry.get("rationale", ""))
|
|
if action == "create_post":
|
|
did_post = True
|
|
elif action == "create_project":
|
|
projs += 1
|
|
elif action == "follow":
|
|
follows += 1
|
|
elif action == "create_gist":
|
|
gists += 1
|
|
await b._idle(0.5, 1.5)
|
|
return actions, did_post, projs, follows, gists
|
|
|
|
async def _cycle_ai(
|
|
self,
|
|
mood: str = "normal",
|
|
*,
|
|
session_posted: bool = False,
|
|
session_projects: int = 0,
|
|
session_follows: int = 0,
|
|
session_gists: int = 0,
|
|
can_project: bool = False,
|
|
can_post: bool = False,
|
|
can_message: bool = False,
|
|
) -> tuple[int, bool, int, int, int]:
|
|
b = self.b
|
|
curl = await b.url()
|
|
page_text = (await b.html()).lower()
|
|
page_state = await self._page_state(
|
|
curl,
|
|
page_text,
|
|
did_post=session_posted,
|
|
follows=session_follows,
|
|
gists=session_gists,
|
|
can_project=can_project,
|
|
can_post=can_post,
|
|
can_message=can_message,
|
|
)
|
|
if page_state["page"] in (
|
|
"post",
|
|
"news_detail",
|
|
"project_detail",
|
|
"gist_detail",
|
|
):
|
|
await self._read_like_human(4, 90)
|
|
menu = self._build_menu(page_state)
|
|
decision = await self._generate(
|
|
self.llm.decide,
|
|
self.state.identity,
|
|
page_state,
|
|
menu,
|
|
self._session_history,
|
|
self._decision_temperature,
|
|
)
|
|
if not decision:
|
|
decision = {"plan": [], "energy": self._session_energy, "stop_after": 0}
|
|
self._session_energy = decision.get("energy", self._session_energy)
|
|
self._session_stop_after = decision.get("stop_after", 0)
|
|
actions, did_post, projs, follows, gists = await self._run_plan(
|
|
decision["plan"], page_state
|
|
)
|
|
return (
|
|
actions,
|
|
did_post or session_posted,
|
|
session_projects + projs,
|
|
session_follows + follows,
|
|
session_gists + gists,
|
|
)
|
|
|
|
def _scaled_pause(self) -> int:
|
|
base = random.randint(self._pause_min, self._pause_max)
|
|
factor = 1.4 - 0.8 * self._session_energy
|
|
return max(1, int(base * factor))
|