fix: correct "bugs" to "issues" in routing table and README references across multiple documentation files
This commit is contained in:
@@ -17,7 +17,6 @@ from devplacepy.services.bot.config import (
|
||||
COMMENT_COOLDOWN_MIN_SECONDS,
|
||||
FEED_TOPICS,
|
||||
GIST_LANGUAGES,
|
||||
HANDLE_SUFFIX_WORDS,
|
||||
MAX_SIBLING_COMMENTS,
|
||||
MAX_THREAD_REPLIES,
|
||||
MENTION_REPLIES_PER_SESSION,
|
||||
@@ -36,6 +35,7 @@ from devplacepy.services.bot.config import (
|
||||
persona_article_score,
|
||||
pick_category,
|
||||
)
|
||||
from devplacepy.services.bot.handles import make_handle
|
||||
from devplacepy.services.bot.llm import LLMClient
|
||||
from devplacepy.services.bot.news_fetcher import NewsFetcher
|
||||
from devplacepy.services.bot.registry import ArticleRegistry
|
||||
@@ -91,7 +91,7 @@ class DevPlaceBot:
|
||||
self._session_mention_replies = 0
|
||||
self._post_cache: list[tuple[str, str, str, str, str]] = []
|
||||
self._project_cache: list[tuple[str, str]] = []
|
||||
self._bug_cache: list[tuple[str, str]] = []
|
||||
self._issue_cache: list[tuple[str, str]] = []
|
||||
self._session_start_cost: float = 0.0
|
||||
self._session_start_calls: int = 0
|
||||
self._session_history: list[str] = []
|
||||
@@ -214,10 +214,10 @@ class DevPlaceBot:
|
||||
self.llm.generate_project_desc, t, self.state.persona
|
||||
)
|
||||
self._project_cache.append((t, desc))
|
||||
elif kind == "bug":
|
||||
elif kind == "issue":
|
||||
topic = random.choice(["UI glitch", "Broken link", "Performance issue"])
|
||||
bug = await asyncio.to_thread(self.llm.generate_bug, topic)
|
||||
self._bug_cache.append(bug)
|
||||
issue = await asyncio.to_thread(self.llm.generate_issue, topic)
|
||||
self._issue_cache.append(issue)
|
||||
except Exception as e:
|
||||
logger.warning("Cache refill (%s) failed: %s", kind, e)
|
||||
|
||||
@@ -225,36 +225,33 @@ class DevPlaceBot:
|
||||
self._log("Warming content cache (background)...")
|
||||
self._post_cache = []
|
||||
self._project_cache = []
|
||||
self._bug_cache = []
|
||||
self._issue_cache = []
|
||||
self.news.fetch()
|
||||
asyncio.create_task(self._refill_cache_async("post"))
|
||||
asyncio.create_task(self._refill_cache_async("project"))
|
||||
asyncio.create_task(self._refill_cache_async("bug"))
|
||||
asyncio.create_task(self._refill_cache_async("issue"))
|
||||
|
||||
def _generate_handle(self) -> str:
|
||||
first = re.sub(r"[^a-z]", "", self.faker.first_name().lower()) or "dev"
|
||||
last = re.sub(r"[^a-z]", "", self.faker.last_name().lower()) or "coder"
|
||||
styles = [
|
||||
first,
|
||||
f"{first}.{last}",
|
||||
f"{first}{last}",
|
||||
f"{first}_{last}",
|
||||
f"{first[0]}{last}",
|
||||
f"{first}{last[0]}",
|
||||
f"{last}{first[0]}",
|
||||
]
|
||||
handle = random.choice(styles)
|
||||
if random.random() < 0.18:
|
||||
word = random.choice(HANDLE_SUFFIX_WORDS)
|
||||
sep = random.choice(["", "_", ".", "-"])
|
||||
handle = f"{handle}{sep}{word}"
|
||||
handle = re.sub(r"[^a-z0-9._-]", "", handle)[:20]
|
||||
return handle or "dev"
|
||||
return make_handle(SEARCH_TERMS.get(self.state.persona, []))
|
||||
|
||||
def _next_handle(self) -> str:
|
||||
while self.state.handle_candidates:
|
||||
candidate = self.state.handle_candidates.pop(0)
|
||||
if candidate:
|
||||
return candidate
|
||||
return self._generate_handle()
|
||||
|
||||
async def _ensure_auth(self) -> bool:
|
||||
b = self.b
|
||||
if not self.state.username:
|
||||
self.state.username = self._generate_handle()
|
||||
if not self.state.handle_candidates:
|
||||
self.state.handle_candidates = (
|
||||
await self._generate(
|
||||
self.llm.generate_handle_candidates, self.state.persona
|
||||
)
|
||||
or []
|
||||
)
|
||||
self.state.username = self._next_handle()
|
||||
self.state.email = self.faker.email()
|
||||
self.state.password = self.faker.password(length=random.randint(12, 18))
|
||||
self.state.started_at = datetime.now().isoformat()
|
||||
@@ -299,14 +296,10 @@ class DevPlaceBot:
|
||||
self._log(f"Registered as {self.state.username}")
|
||||
return True
|
||||
if attempt >= 2:
|
||||
base = self._generate_handle()
|
||||
word = random.choice(HANDLE_SUFFIX_WORDS)
|
||||
sep = random.choice(["", "_", ".", "-"])
|
||||
self.state.username = re.sub(
|
||||
r"[^a-z0-9._-]", "", f"{base}{sep}{word}"
|
||||
)[:20]
|
||||
base = self._next_handle()
|
||||
self.state.username = f"{base}{random.randint(10, 9999)}"[:20]
|
||||
else:
|
||||
self.state.username = self._generate_handle()
|
||||
self.state.username = self._next_handle()
|
||||
self.state.email = self.faker.email()
|
||||
self._log(f"Signup failed, retrying as {self.state.username}")
|
||||
return False
|
||||
@@ -1794,15 +1787,15 @@ class DevPlaceBot:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _report_bug(self) -> bool:
|
||||
async def _report_issue(self) -> bool:
|
||||
b = self.b
|
||||
await b.goto(f"{self.base_url}/bugs")
|
||||
ok = await b.click("[data-modal='create-bug-modal']")
|
||||
await b.goto(f"{self.base_url}/issues")
|
||||
ok = await b.click("[data-modal='create-issue-modal']")
|
||||
if not ok:
|
||||
return False
|
||||
await b._idle(0.5, 1.5)
|
||||
|
||||
if not await b.text("#create-bug-modal"):
|
||||
if not await b.text("#create-issue-modal"):
|
||||
return False
|
||||
|
||||
topic = random.choice(
|
||||
@@ -1814,27 +1807,27 @@ class DevPlaceBot:
|
||||
"Formatting error",
|
||||
]
|
||||
)
|
||||
if self._bug_cache:
|
||||
title, desc = self._bug_cache.pop(0)
|
||||
asyncio.create_task(self._refill_cache_async("bug"))
|
||||
if self._issue_cache:
|
||||
title, desc = self._issue_cache.pop(0)
|
||||
asyncio.create_task(self._refill_cache_async("issue"))
|
||||
else:
|
||||
generated = await self._generate(self.llm.generate_bug, topic)
|
||||
generated = await self._generate(self.llm.generate_issue, topic)
|
||||
if not generated:
|
||||
self._log("Report bug: generation failed")
|
||||
self._log("Report issue: generation failed")
|
||||
return False
|
||||
title, desc = generated
|
||||
|
||||
await b.fill("#bug-title", title[:200])
|
||||
await b.fill("#issue-title", title[:200])
|
||||
await b._idle(0.2, 0.5)
|
||||
await b.fill("textarea[name='description']", desc[:2000])
|
||||
await b._idle(0.5, 1.0)
|
||||
|
||||
ok = await b.click(
|
||||
"button:has-text('Submit Report'), #create-bug-modal .btn-primary"
|
||||
"button:has-text('Submit Report'), #create-issue-modal .btn-primary"
|
||||
)
|
||||
if ok:
|
||||
self.state.bugs_filed += 1
|
||||
self._action("BUG", f"{title[:50]} [{self.state.bugs_filed}]")
|
||||
self.state.issues_filed += 1
|
||||
self._action("ISSUE", f"{title[:50]} [{self.state.issues_filed}]")
|
||||
await asyncio.sleep(2)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import random
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from devplacepy.services.bot.config import HANDLE_SUFFIX_WORDS
|
||||
|
||||
MIN_HANDLE_LEN = 3
|
||||
MAX_HANDLE_LEN = 20
|
||||
MIN_SEED_TOKEN_LEN = 3
|
||||
LEET_PROBABILITY = 0.35
|
||||
LEET_CHAR_PROBABILITY = 0.5
|
||||
NUMBER_SUFFIX_PROBABILITY = 0.30
|
||||
SEPARATOR_PROBABILITY = 0.30
|
||||
CAPITALIZE_PROBABILITY = 0.22
|
||||
ALLCAPS_PROBABILITY = 0.08
|
||||
ALLCAPS_MAX_LEN = 8
|
||||
PAD_NUMBER_FLOOR = 10
|
||||
PAD_NUMBER_CEIL = 9999
|
||||
|
||||
LEET_MAP = {
|
||||
"a": "4",
|
||||
"e": "3",
|
||||
"i": "1",
|
||||
"o": "0",
|
||||
"s": "5",
|
||||
"t": "7",
|
||||
"l": "1",
|
||||
"g": "9",
|
||||
"b": "8",
|
||||
}
|
||||
|
||||
TECH_NOUNS = [
|
||||
"byte", "bit", "nibble", "null", "void", "root", "kernel", "daemon",
|
||||
"socket", "buffer", "cache", "stack", "heap", "pointer", "segfault",
|
||||
"regex", "lambda", "vector", "packet", "proxy", "token", "hash", "cipher",
|
||||
"vortex", "flux", "glitch", "pixel", "neon", "cyber", "quantum", "thread",
|
||||
"mutex", "syntax", "binary", "octet", "raster", "shader", "codec",
|
||||
"vertex", "matrix", "scalar", "payload", "opcode", "runtime", "compiler",
|
||||
"parser", "lexer", "sandbox", "firmware", "bytecode", "checksum",
|
||||
"entropy", "signal", "overflow", "nibbler", "cursor", "patch",
|
||||
]
|
||||
|
||||
HANDLE_ADJECTIVES = [
|
||||
"dark", "mad", "lazy", "quantum", "cyber", "neon", "ghost", "hyper",
|
||||
"ultra", "neo", "retro", "raw", "zero", "async", "blind", "silent",
|
||||
"atomic", "fatal", "rogue", "idle", "stale", "dirty", "lucid", "prime",
|
||||
"hex", "cold", "lone", "wired", "broken", "feral",
|
||||
]
|
||||
|
||||
HANDLE_CREATURES = [
|
||||
"fox", "wolf", "raven", "owl", "octopus", "gecko", "mantis", "viper",
|
||||
"lynx", "moth", "hydra", "kraken", "phoenix", "dragon", "falcon",
|
||||
"panther", "cobra", "badger", "otter", "sloth", "hornet", "beetle",
|
||||
"lemur", "narwhal", "basilisk",
|
||||
]
|
||||
|
||||
HANDLE_PREFIXES = ["the", "mr", "dr", "lord", "captain", "uncle", "sir", "prof", "agent"]
|
||||
|
||||
NUMBER_SUFFIXES = ["42", "1337", "404", "64", "99", "007", "23", "88", "256", "512", "101", "777"]
|
||||
|
||||
SEPARATORS = ["_", "-"]
|
||||
|
||||
|
||||
def _join(rng: random.Random, parts: list[str]) -> str:
|
||||
separator = rng.choice(SEPARATORS) if rng.random() < SEPARATOR_PROBABILITY else ""
|
||||
return separator.join(parts)
|
||||
|
||||
|
||||
def _seed_tokens(interests: list[str]) -> list[str]:
|
||||
tokens: list[str] = []
|
||||
for term in interests:
|
||||
for piece in re.split(r"[^a-z0-9]+", term.lower()):
|
||||
if len(piece) >= MIN_SEED_TOKEN_LEN:
|
||||
tokens.append(piece)
|
||||
return tokens
|
||||
|
||||
|
||||
def _build_core(rng: random.Random, interests: list[str]) -> str:
|
||||
seeds = _seed_tokens(interests)
|
||||
builders = [
|
||||
lambda: rng.choice(TECH_NOUNS),
|
||||
lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(TECH_NOUNS)]),
|
||||
lambda: _join(rng, [rng.choice(TECH_NOUNS), rng.choice(HANDLE_CREATURES)]),
|
||||
lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(HANDLE_CREATURES)]),
|
||||
lambda: _join(rng, [rng.choice(HANDLE_PREFIXES), rng.choice(TECH_NOUNS + HANDLE_CREATURES)]),
|
||||
lambda: _join(rng, [rng.choice(TECH_NOUNS), rng.choice(HANDLE_SUFFIX_WORDS)]),
|
||||
]
|
||||
weights = [3, 4, 3, 2, 2, 2]
|
||||
if seeds:
|
||||
builders.append(
|
||||
lambda: _join(rng, [rng.choice(seeds), rng.choice(HANDLE_SUFFIX_WORDS + TECH_NOUNS)])
|
||||
)
|
||||
builders.append(lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(seeds)]))
|
||||
weights += [5, 3]
|
||||
return rng.choices(builders, weights=weights)[0]()
|
||||
|
||||
|
||||
def apply_leet(text: str, rng: random.Random) -> str:
|
||||
chars: list[str] = []
|
||||
for char in text:
|
||||
replacement = LEET_MAP.get(char.lower())
|
||||
if replacement and rng.random() < LEET_CHAR_PROBABILITY:
|
||||
chars.append(replacement)
|
||||
else:
|
||||
chars.append(char)
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def _apply_case(text: str, rng: random.Random) -> str:
|
||||
roll = rng.random()
|
||||
if roll < ALLCAPS_PROBABILITY and len(text) <= ALLCAPS_MAX_LEN:
|
||||
return text.upper()
|
||||
if roll < ALLCAPS_PROBABILITY + CAPITALIZE_PROBABILITY:
|
||||
return text[:1].upper() + text[1:]
|
||||
return text
|
||||
|
||||
|
||||
def sanitize_handle(raw: str) -> str:
|
||||
handle = re.sub(r"[^a-zA-Z0-9_-]", "", str(raw).strip())
|
||||
handle = re.sub(r"[_-]{2,}", lambda match: match.group(0)[0], handle).strip("_-")
|
||||
return handle[:MAX_HANDLE_LEN].rstrip("_-")
|
||||
|
||||
|
||||
def make_handle(interests: Optional[list[str]] = None, rng: Optional[random.Random] = None) -> str:
|
||||
generator = rng or random
|
||||
core = _build_core(generator, interests or [])
|
||||
if generator.random() < LEET_PROBABILITY:
|
||||
core = apply_leet(core, generator)
|
||||
if generator.random() < NUMBER_SUFFIX_PROBABILITY:
|
||||
core = f"{core}{generator.choice(NUMBER_SUFFIXES)}"
|
||||
handle = sanitize_handle(core)
|
||||
if len(handle) < MIN_HANDLE_LEN:
|
||||
handle = f"{handle}{generator.randint(PAD_NUMBER_FLOOR, PAD_NUMBER_CEIL)}"
|
||||
handle = _apply_case(handle, generator)
|
||||
return handle or f"dev{generator.randint(PAD_NUMBER_FLOOR, PAD_NUMBER_CEIL)}"
|
||||
@@ -5,10 +5,11 @@ import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from devplacepy import stealth
|
||||
from devplacepy.services.bot.config import (
|
||||
GIST_LANGUAGES,
|
||||
GIST_MIN_LINES,
|
||||
@@ -17,6 +18,9 @@ from devplacepy.services.bot.config import (
|
||||
SEARCH_TERMS,
|
||||
TRIVIAL_GIST_TERMS,
|
||||
)
|
||||
from devplacepy.services.bot.handles import MAX_HANDLE_LEN, sanitize_handle
|
||||
|
||||
HANDLE_CANDIDATE_TARGET = 8
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,19 +65,15 @@ class LLMClient:
|
||||
json.dumps(system[:500]),
|
||||
json.dumps(prompt[:500]),
|
||||
)
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
self.api_url,
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
raw = resp.read()
|
||||
with stealth.stealth_sync_client(timeout=30) as client:
|
||||
resp = client.post(
|
||||
self.api_url,
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
)
|
||||
raw = resp.content
|
||||
logger.info("LLM <<< %s", raw[:2000].decode(errors="replace"))
|
||||
result = json.loads(raw)
|
||||
result = resp.json()
|
||||
usage = result.get("usage", {})
|
||||
in_tokens = usage.get("prompt_tokens", 0)
|
||||
out_tokens = usage.get("completion_tokens", 0)
|
||||
@@ -86,8 +86,7 @@ class LLMClient:
|
||||
self.total_out_tokens += out_tokens
|
||||
return result["choices"][0]["message"]["content"]
|
||||
except (
|
||||
urllib.error.HTTPError,
|
||||
urllib.error.URLError,
|
||||
httpx.HTTPError,
|
||||
json.JSONDecodeError,
|
||||
KeyError,
|
||||
) as e:
|
||||
@@ -126,7 +125,7 @@ class LLMClient:
|
||||
"snippet",
|
||||
"headline",
|
||||
"gist",
|
||||
"bug",
|
||||
"issue",
|
||||
)
|
||||
result = (text or "").strip().strip('"').strip("'").strip()
|
||||
changed = True
|
||||
@@ -476,6 +475,38 @@ class LLMClient:
|
||||
data = self._parse_json(self._raw_call(system, prompt, temperature=0.9))
|
||||
return self._normalize_identity(data, archetype)
|
||||
|
||||
def generate_handle_candidates(self, archetype: str) -> list[str]:
|
||||
interests = ", ".join(SEARCH_TERMS.get(archetype, []))
|
||||
system = (
|
||||
"You invent online handles for a developer signing up to a programmer "
|
||||
"community in the style of devRant or Hacker News. The handles read like a "
|
||||
"real nerd picked them, never like a person's full name. Lean on programming "
|
||||
"and hacker culture: tech nouns (null, kernel, segfault, daemon), occasional "
|
||||
"leetspeak (c0d3r, h4x), an adjective plus noun, a creature, or a short word "
|
||||
f"with a number. Lowercase mostly, {MAX_HANDLE_LEN} characters or fewer, only "
|
||||
"letters, digits, underscores and hyphens, no spaces and no dots. Return ONLY "
|
||||
f'a JSON object {{"handles": ["...", "..."]}} with {HANDLE_CANDIDATE_TARGET} '
|
||||
"distinct handles, no prose, no markdown fences. No em dashes."
|
||||
)
|
||||
prompt = (
|
||||
f"Archetype: {archetype}\n"
|
||||
f"Their interests: {interests}\n"
|
||||
"Create the handles JSON:"
|
||||
)
|
||||
try:
|
||||
data = self._parse_json(
|
||||
self._raw_call(system, prompt, temperature=1.0)
|
||||
)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
logger.warning("Handle candidate generation failed: %s", exc)
|
||||
return []
|
||||
handles: list[str] = []
|
||||
for raw in data.get("handles", []):
|
||||
handle = sanitize_handle(raw)
|
||||
if handle and handle.lower() not in {h.lower() for h in handles}:
|
||||
handles.append(handle)
|
||||
return handles
|
||||
|
||||
@staticmethod
|
||||
def _identity_card(identity: dict) -> str:
|
||||
lines = [
|
||||
@@ -586,13 +617,13 @@ class LLMClient:
|
||||
break
|
||||
return result
|
||||
|
||||
def generate_bug(self, topic: str) -> tuple[str, str]:
|
||||
def generate_issue(self, topic: str) -> tuple[str, str]:
|
||||
text = self._call(
|
||||
"Write a bug report. One line title. Then 2-3 sentences describing what happened and what should have happened. Like a real dev reporting a bug. No em dashes.",
|
||||
f"Bug: {topic}",
|
||||
"Write an issue report. One line title. Then 2-3 sentences describing what happened and what should have happened. Like a real dev reporting an issue. No em dashes.",
|
||||
f"Issue: {topic}",
|
||||
)
|
||||
lines = text.strip().split("\n")
|
||||
title = lines[0].strip().lstrip("#").strip()[:120] or f"Bug report: {topic}"
|
||||
title = lines[0].strip().lstrip("#").strip()[:120] or f"Issue report: {topic}"
|
||||
desc = " ".join(
|
||||
line.lstrip("-* ").strip() for line in lines[1:] if line.strip()
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from devplacepy import stealth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,11 +23,9 @@ class NewsFetcher:
|
||||
if fresh:
|
||||
return self.articles
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.news_api, headers={"User-Agent": "dpbot/1.0"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
articles = json.loads(resp.read()).get("articles", [])
|
||||
with stealth.stealth_sync_client(timeout=10) as client:
|
||||
resp = client.get(self.news_api)
|
||||
articles = resp.json().get("articles", [])
|
||||
if articles:
|
||||
self.articles = articles
|
||||
self.fetched_at = time.monotonic()
|
||||
|
||||
@@ -15,6 +15,7 @@ class BotState:
|
||||
password: str = ""
|
||||
uid: str = ""
|
||||
persona: str = ""
|
||||
handle_candidates: list[str] = field(default_factory=list)
|
||||
profile_filled: bool = False
|
||||
user_agent: str = ""
|
||||
viewport: dict = field(default_factory=dict)
|
||||
@@ -27,7 +28,7 @@ class BotState:
|
||||
comments_posted: int = 0
|
||||
quality_rejections: int = 0
|
||||
votes_cast: int = 0
|
||||
bugs_filed: int = 0
|
||||
issues_filed: int = 0
|
||||
projects_created: int = 0
|
||||
gists_created: int = 0
|
||||
profiles_viewed: int = 0
|
||||
|
||||
Reference in New Issue
Block a user