feat: add /stop and /reset chat commands and bots internals docs section

Add two new chat commands (`/stop` and `/reset`) to the Devii WebSocket handler, enabling users to stop or reset a session via text input. Introduce a new "Bots internals" documentation section with six prose pages covering architecture, personas, content generation, engagement, realism, and configuration for the autonomous bot fleet. Extend the bot service with article scoring, category picking, configurable pause/break timing, and a `gist_min_lines` parameter for LLM client initialization.
This commit is contained in:
2026-06-11 12:06:17 +00:00
parent 3540bc8fa6
commit 5488008216
37 changed files with 2576 additions and 340 deletions
+137 -37
View File
@@ -6,7 +6,13 @@ import time
import urllib.error
import urllib.request
from devplacepy.services.bot.config import CATEGORIES, GIST_LANGUAGES
from devplacepy.services.bot.config import (
GIST_LANGUAGES,
GIST_MIN_LINES,
PERSONA_GIST_FLAVOR,
PERSONA_LANGUAGES,
TRIVIAL_GIST_TERMS,
)
logger = logging.getLogger(__name__)
@@ -19,6 +25,7 @@ class LLMClient:
model: str,
input_cost_per_1m: float,
output_cost_per_1m: float,
gist_min_lines: int = GIST_MIN_LINES,
):
if not api_key:
raise RuntimeError("LLM API key not set")
@@ -27,6 +34,7 @@ class LLMClient:
self.model = model
self.input_cost_per_1m = input_cost_per_1m
self.output_cost_per_1m = output_cost_per_1m
self.gist_min_lines = max(1, gist_min_lines)
self.total_cost = 0.0
self.total_calls = 0
self.total_in_tokens = 0
@@ -89,7 +97,7 @@ class LLMClient:
def clean(text: str, preserve_md: bool = False) -> str:
if not preserve_md:
text = re.sub(r"\*\*|__|\*|_", "", text)
text = text.replace("-", "-").replace("", "-")
text = text.replace("", "-").replace("", "-")
text = text.replace("", "'").replace("", "'")
text = text.replace("", '"').replace("", '"')
text = re.sub(r"\s{2,}", " ", text)
@@ -99,6 +107,36 @@ class LLMClient:
def strip_md(text: str) -> str:
return LLMClient.clean(text)[:200]
@staticmethod
def strip_label(text: str) -> str:
labels = (
"post title",
"project name",
"snippet name",
"title",
"name",
"concept",
"snippet",
"headline",
"gist",
"bug",
)
result = (text or "").strip().strip('"').strip("'").strip()
changed = True
while changed:
changed = False
lowered = result.lower()
for label in labels:
if lowered.startswith(label + ":"):
result = result[len(label) + 1 :].strip().strip('"').strip("'")
changed = True
break
for sep in (" concept:", " description:", " desc:", " idea:"):
idx = result.lower().find(sep)
if idx > 0:
result = result[:idx].strip()
return result.strip().strip('"').strip("'").strip()
@staticmethod
def strip_code_fences(text: str) -> str:
text = text.strip()
@@ -154,16 +192,33 @@ class LLMClient:
)
return self.clean(text, preserve_md=preserve)
def select_category(self, title: str, desc: str) -> str:
text = self._call(
"You are a classifier. Given tech news, choose the single best post category. Reply with ONLY the category name.",
f"Categories:\n- devlog: learning journey, building something, personal dev experience\n- showcase: impressive release, new tool, achievement worth highlighting\n- question: uncertainty, asking for advice, curiosity about implications\n- rant: frustration, bad practices, criticism of industry trends\n- fun: amusing, surprising, entertaining but not deeply serious\n- random: anything else that doesn't clearly fit above\n\nNews: {title}\n\n{desc[:600]}",
temperature=0.2,
def generate_post_title(
self, headline: str, persona: str = "", category: str = ""
) -> str:
style = {
"enthusiastic_junior": "Sound excited and curious.",
"grumpy_senior": "Sound blunt and a little skeptical.",
"hobbyist_maker": "Sound casual and hands-on.",
"academic_type": "Sound precise and measured.",
"minimalist": "Keep it plain and very short.",
"storyteller": "Hint at a story or an angle.",
"rebel": "Sound provocative or contrarian.",
"mentor": "Sound thoughtful and constructive.",
}.get(persona, "")
title = self.strip_label(
self.clean(
self._call(
"You are a developer writing the title of a community forum post reacting to tech news. "
"Write a natural, human title of 3 to 8 words in your own voice. "
"Do not copy or paraphrase the full headline and do not restate every detail. "
"No trailing punctuation unless it is a genuine question. No quotes. No label prefix. "
f"No em dashes. {style}",
f"Headline: {headline}\nYour post title:",
temperature=0.8,
)
)
)
for c in CATEGORIES:
if c in text.lower():
return c
return "random"
return title[:120]
def select_reaction(self, content_snippet: str, persona: str = "") -> str:
from devplacepy.constants import REACTION_EMOJI
@@ -334,40 +389,85 @@ class LLMClient:
)
)[:500]
def generate_project_title(self) -> str:
return self._call(
"Come up with a project name. 2-4 words. A tool, game, or app idea. No em dashes.",
"Name:",
temperature=0.9,
)
def generate_project_title(self, persona: str = "") -> str:
return self.strip_label(
self.clean(
self._call(
"Invent a short, catchy project name. 2 to 4 words. A tool, game, or app. "
"Output only the name. No label, no colon, no description, no quotes. No em dashes.",
"Project name:",
temperature=0.9,
)
)
)[:80]
def generate_project_desc(self, title: str) -> str:
return self._call(
"You are a developer. Write a compelling 2-3 sentence project description including tech stack and purpose.",
f"Project: {title}",
)
def generate_project_desc(self, title: str, persona: str = "") -> str:
flavor = {
"grumpy_senior": "Keep it dry and matter of fact.",
"academic_type": "Be precise about the approach and trade-offs.",
"minimalist": "Two short sentences, no filler.",
"rebel": "Be bold about why the usual approach is wrong.",
"storyteller": "Open with the motivation behind it.",
}.get(persona, "")
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"Project: {title}",
)
)[:5000]
def generate_gist(self, persona: str = "") -> tuple[str, str, str, str]:
language = random.choice(GIST_LANGUAGES)
title = self.clean(
languages = PERSONA_LANGUAGES.get(persona) or GIST_LANGUAGES
language = random.choice(languages)
flavor = PERSONA_GIST_FLAVOR.get(persona, "a genuinely useful utility")
code = self.strip_code_fences(
self._call(
"Name a short, useful code snippet. 2-5 words. No quotes. No markdown.",
f"Language: {language}. Snippet name:",
temperature=0.9,
f"Write a short, correct, self-contained {language} snippet of 8 to 20 lines. "
f"It should be {flavor}. Make it non-trivial and genuinely useful: no hello world, "
"no bare language-feature demo, no textbook 101 example, no trivial one-liner, "
"no basic getter or setter. Output ONLY raw code. No markdown fences. No commentary.",
f"Language: {language}. Write the snippet:",
temperature=0.7,
)
)[:4000]
title = self.strip_label(
self.clean(
self._call(
"Name this code snippet in 2 to 5 words, like a developer titling a gist. "
"No quotes. No markdown. No label prefix. No em dashes.",
f"Language: {language}\nCode:\n{code[:1200]}\nTitle:",
temperature=0.7,
)
)
)[:120]
description = self.clean(
self._call(
"Write a one-sentence description of what a code snippet does, like a dev sharing something handy. No em dashes.",
f"Snippet: {title}\nLanguage: {language}",
"Write a one-sentence description of what this snippet does and when it is handy. No em dashes.",
f"Title: {title}\nLanguage: {language}\nCode:\n{code[:1200]}",
)
)[:400]
code = self.strip_code_fences(
self._call(
f"Write a short, correct, self-contained {language} snippet of 5 to 20 lines for the description. "
"Output ONLY raw code. No markdown fences. No commentary.",
f"Title: {title}\nDescription: {description}\nLanguage: {language}",
temperature=0.4,
)
)[:4000]
return title, description, language, code
def gist_quality_check(
self, title: str, code: str, language: str
) -> tuple[bool, str]:
lowered = (title or "").lower()
for term in TRIVIAL_GIST_TERMS:
if term in lowered:
return False, f"trivial topic '{term}'"
lines = [ln for ln in (code or "").splitlines() if ln.strip()]
if len(lines) < self.gist_min_lines:
return False, f"too few lines ({len(lines)} < {self.gist_min_lines})"
verdict = self._call(
"You are a strict reviewer for a developer community's shared code snippets. "
"Reject snippets that are textbook 101 material, trivial one-liners, hello world, "
"a bare language-feature demo, or something every developer already knows by heart. "
"Accept only snippets an experienced developer would find non-obvious or worth bookmarking. "
"Reply with exactly PASS or 'FAIL: <short reason>'.",
f"Language: {language}\nTitle: {title}\nCode:\n{code[:1500]}",
temperature=0.0,
)
if verdict.strip().upper().startswith("PASS"):
return True, "ok"
return False, verdict.strip()[:120] or "judge rejected"