feat: add startup jitter, randomized browser fingerprints, and comment style variants to bot fleet

Introduce `startup_jitter_seconds` config field so each bot delays its first session by a random amount up to the configured maximum, spreading fleet activity on service start. Persist a randomly chosen user-agent and viewport per bot in `BotState` and pass them to `BotBrowser` constructor, replacing the hardcoded defaults with a varied pool defined in `config.py`. Add `HANDLE_SUFFIX_WORDS` list and remove numeric suffixes from handle generation to avoid bot-farm appearance. Implement `pick_comment_style` and `is_short_comment_style` static methods in `LLMClient`, extend `generate_comment` with a `style` parameter that produces one-liner or question variants using `MIN_SHORT_COMMENT_LEN`, and wire session comment caps (`SESSION_COMMENT_CAP_MIN/MAX`) with cooldown intervals (`COMMENT_COOLDOWN_MIN/MAX_SECONDS`) into the bot loop. Update `BotRuntimeConfig`, `BotsService` config field registration, and architecture docs to reflect the new identity and pacing logic.
This commit is contained in:
2026-06-13 11:19:32 +00:00
parent 89635dd7e1
commit ce3cae1433
11 changed files with 226 additions and 35 deletions
+47 -5
View File
@@ -256,12 +256,25 @@ class LLMClient:
return emoji
return ""
@staticmethod
def pick_comment_style() -> str:
return random.choices(
["standard", "oneliner", "question"],
weights=[55, 23, 22],
k=1,
)[0]
@staticmethod
def is_short_comment_style(style: str) -> bool:
return style in ("oneliner", "question")
def generate_comment(
self,
post_snippet: str,
persona: str = "",
mention_target: str = "",
parent_context: str = "",
style: str = "",
) -> str:
extras = {
"enthusiastic_junior": "Be excited. Use **bold** for agreement. Short replies.",
@@ -277,6 +290,20 @@ class LLMClient:
else " Be casual. No markdown."
)
preserve = persona in ("enthusiastic_junior", "mentor", "storyteller")
if style == "oneliner":
length_rule = (
"Write ONE short casual line, under 12 words, like a quick reply "
"you would type without thinking too hard. Lowercase is fine, "
"contractions are fine, no markdown."
)
preserve = False
elif style == "question":
length_rule = (
"Write a SINGLE pointed question about a specific detail in the post. "
"One sentence. No preamble."
)
else:
length_rule = f"Write 1-3 short sentences.{extra}"
mention_rule = ""
if mention_target:
mention_rule = (
@@ -287,7 +314,7 @@ class LLMClient:
if parent_context:
ctx = f"\n\nReplying to this comment:\n{parent_context[:800]}"
system = (
f"You are a dev replying to a post. Write 1-3 short sentences.{extra}{mention_rule} "
f"You are a dev replying to a post. {length_rule}{mention_rule} "
"No em dashes. Reference a specific detail from the post. "
"Do not open with or rely on generic filler like 'great point', 'I agree', 'nice', "
"'thanks for sharing', or 'interesting'. Add something real: a concrete experience, "
@@ -313,17 +340,25 @@ class LLMClient:
return hits / len(candidate)
def quality_check(
self, kind: str, text: str, context: str = ""
self, kind: str, text: str, context: str = "", style: str = ""
) -> tuple[bool, str]:
from devplacepy.services.bot.config import (
MIN_COMMENT_LEN,
MIN_SHORT_COMMENT_LEN,
MIN_POST_LEN,
RESTATEMENT_OVERLAP_THRESHOLD,
GENERIC_COMMENT_PHRASES,
)
stripped = self.clean(text or "").strip()
min_len = MIN_COMMENT_LEN if kind == "comment" else MIN_POST_LEN
if kind == "comment":
min_len = (
MIN_SHORT_COMMENT_LEN
if self.is_short_comment_style(style)
else MIN_COMMENT_LEN
)
else:
min_len = MIN_POST_LEN
if len(stripped) < min_len:
return False, f"too short ({len(stripped)} < {min_len})"
lowered = stripped.lower()
@@ -593,10 +628,17 @@ class LLMClient:
"rebel": "Be bold about why the usual approach is wrong.",
"storyteller": "Open with the motivation behind it.",
}.get(persona, "")
shape = random.choice(
[
"Write one terse sentence. Just what it is. No tech stack.",
"Write two sentences: what it does and the main tech behind it.",
"Write 2 to 3 sentences including the tech stack and a trade-off you made.",
"Write a short blurb, under 25 words, plain and understated.",
]
)
return self.clean(
self._call(
"You are a developer. Write a compelling 2 to 3 sentence project description "
f"including the tech stack and purpose. {flavor} No em dashes.",
f"You are a developer describing your own project. {shape} {flavor} No em dashes.",
f"Project: {title}",
)
)[:5000]