chore: add pretty_json utility and sibling comment awareness to bot realism mechanics

This commit is contained in:
2026-06-12 06:30:17 +00:00
parent ce3cae1433
commit c1f314f2c6
7 changed files with 112 additions and 8 deletions
+2 -1
View File
@@ -46,6 +46,7 @@ from devplacepy.utils import (
get_current_user,
is_admin,
not_found,
pretty_json,
)
from devplacepy.seo import base_seo_context, site_url, website_schema
from devplacepy.responses import respond, action_result
@@ -743,7 +744,7 @@ async def admin_audit_event(request: Request, uid: str):
)
event["metadata"] = _parse_metadata(event.get("metadata"))
event["metadata_pretty"] = (
json.dumps(event["metadata"], indent=2) if event["metadata"] else ""
pretty_json(event["metadata"]) if event["metadata"] else ""
)
event["time_ago"] = (
time_ago(event["created_at"]) if event.get("created_at") else ""
+49 -1
View File
@@ -18,6 +18,7 @@ from devplacepy.services.bot.config import (
FEED_TOPICS,
GIST_LANGUAGES,
HANDLE_SUFFIX_WORDS,
MAX_SIBLING_COMMENTS,
PERSONAS,
PROJECT_STATUSES,
PROJECT_TYPES,
@@ -26,6 +27,7 @@ from devplacepy.services.bot.config import (
SEARCH_TERMS,
SESSION_COMMENT_CAP_MAX,
SESSION_COMMENT_CAP_MIN,
SIBLING_SNIPPET_LEN,
USER_AGENTS,
VIEWPORTS,
persona_article_score,
@@ -829,6 +831,39 @@ class DevPlaceBot:
self.state.own_post_urls.append(curl)
return is_own
async def _existing_comments(self) -> list[str]:
bodies = self.b.page.locator(".comment-body")
try:
count = await bodies.count()
except Exception:
return []
if count == 0:
return []
own = self.state.username.lower()
collected: list[str] = []
start = max(0, count - MAX_SIBLING_COMMENTS)
for idx in range(start, count):
node = bodies.nth(idx)
try:
text = (await node.locator(".comment-text").first.inner_text()).strip()
except Exception:
continue
if not text:
continue
author = ""
try:
link = node.locator("a[href*='/profile/']").first
if await link.count() > 0:
href = await link.get_attribute("href") or ""
author = href.split("/profile/")[-1].split("?")[0].split("/")[0]
except Exception:
author = ""
if author and author.lower() == own:
continue
snippet = text[:SIBLING_SNIPPET_LEN]
collected.append(f"{author}: {snippet}" if author else snippet)
return collected
async def _comment_on_post(self) -> bool:
b = self.b
curl = await b.url()
@@ -891,6 +926,10 @@ class DevPlaceBot:
mention_target = random.choice(known)
style = self.llm.pick_comment_style()
siblings = await self._existing_comments()
siblings_block = "\n".join(f"- {c}" for c in siblings)
siblings_text = " ".join(siblings)
comment = ""
for attempt in range(2):
candidate = await self._generate(
@@ -900,11 +939,17 @@ class DevPlaceBot:
mention_target,
"",
style,
siblings_block,
)
if not candidate:
continue
verdict = await self._generate(
self.llm.quality_check, "comment", candidate, post_body[:1500], style
self.llm.quality_check,
"comment",
candidate,
post_body[:1500],
style,
siblings_text,
)
if verdict is None or verdict[0]:
comment = candidate
@@ -1039,6 +1084,8 @@ class DevPlaceBot:
logger.debug("reply-btn click failed: %s", e)
continue
post_body = await b.text(".post-content") or await b.text("article") or ""
siblings = [c for c in await self._existing_comments() if text[:60] not in c]
siblings_block = "\n".join(f"- {c}" for c in siblings)
reply = await self._generate(
self.llm.generate_comment,
post_body[:1200],
@@ -1046,6 +1093,7 @@ class DevPlaceBot:
mentioner,
text,
self.llm.pick_comment_style(),
siblings_block,
)
if not reply:
self._log("Reply: generation failed/empty")
+3
View File
@@ -134,6 +134,9 @@ MIN_COMMENT_LEN = 40
MIN_SHORT_COMMENT_LEN = 12
MIN_POST_LEN = 120
RESTATEMENT_OVERLAP_THRESHOLD = 0.6
SIBLING_OVERLAP_THRESHOLD = 0.55
MAX_SIBLING_COMMENTS = 8
SIBLING_SNIPPET_LEN = 240
GENERIC_COMMENT_PHRASES = [
"great point",
+23 -3
View File
@@ -275,6 +275,7 @@ class LLMClient:
mention_target: str = "",
parent_context: str = "",
style: str = "",
existing_comments: str = "",
) -> str:
extras = {
"enthusiastic_junior": "Be excited. Use **bold** for agreement. Short replies.",
@@ -313,16 +314,29 @@ class LLMClient:
ctx = ""
if parent_context:
ctx = f"\n\nReplying to this comment:\n{parent_context[:800]}"
others = ""
distinct_rule = ""
if existing_comments:
others = (
"\n\nComments already posted by others on this thread:\n"
f"{existing_comments[:1600]}"
)
distinct_rule = (
" Other people already commented (listed below). Contribute a point "
"none of them made: a different angle on the topic, a caveat they "
"missed, or respectful disagreement with one of them by name. Do not "
"repeat any opinion, framing, example, or question already raised."
)
system = (
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, "
"a caveat, a counterexample, respectful disagreement, or a pointed question. "
"Never just paraphrase or agree blandly."
f"Never just paraphrase or agree blandly.{distinct_rule}"
)
reply = self.clean(
self._call(system, f"Post:\n{post_snippet[:1500]}{ctx}").strip(),
self._call(system, f"Post:\n{post_snippet[:1500]}{ctx}{others}").strip(),
preserve_md=preserve,
)
return reply[:2000]
@@ -340,13 +354,14 @@ class LLMClient:
return hits / len(candidate)
def quality_check(
self, kind: str, text: str, context: str = "", style: str = ""
self, kind: str, text: str, context: str = "", style: str = "", siblings: str = ""
) -> tuple[bool, str]:
from devplacepy.services.bot.config import (
MIN_COMMENT_LEN,
MIN_SHORT_COMMENT_LEN,
MIN_POST_LEN,
RESTATEMENT_OVERLAP_THRESHOLD,
SIBLING_OVERLAP_THRESHOLD,
GENERIC_COMMENT_PHRASES,
)
@@ -371,6 +386,11 @@ class LLMClient:
and self._overlap_ratio(stripped, context) > RESTATEMENT_OVERLAP_THRESHOLD
):
return False, "restates the source"
if (
siblings
and self._overlap_ratio(stripped, siblings) > SIBLING_OVERLAP_THRESHOLD
):
return False, "echoes another comment"
verdict = self._call(
"You are a strict content quality reviewer for a developer community. "
"Reject text that is generic, low-effort, pure filler, or merely summarizes its "
+2 -1
View File
@@ -8,7 +8,7 @@ from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.database import get_int_setting, get_setting, get_table
from devplacepy.avatar import avatar_url
from devplacepy.utils import format_date as _format_date
from devplacepy.utils import get_badge, is_admin
from devplacepy.utils import get_badge, is_admin, pretty_json
from devplacepy.attachments import format_file_size, file_icon_emoji
from devplacepy.content import is_owner as _owns
from devplacepy.customization import custom_css_tag, custom_js_tag, page_type_for
@@ -135,6 +135,7 @@ def jinja_language_name(code: str) -> str:
templates.env.globals["language_name"] = jinja_language_name
templates.env.globals["pretty_json"] = pretty_json
templates.env.globals["format_file_size"] = format_file_size
templates.env.globals["file_icon_emoji"] = file_icon_emoji
+30
View File
@@ -5,10 +5,12 @@ import base64
import binascii
import hashlib
import html
import json
import re
import secrets
import logging
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from passlib.hash import pbkdf2_sha256
from fastapi import Request, HTTPException, status
from devplacepy.cache import TTLCache
@@ -263,6 +265,34 @@ def strip_html(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
_FLOAT_TOKEN = "@dpfloat@"
def plain_float(value: float) -> str:
if value != value or value in (float("inf"), float("-inf")):
return json.dumps(value)
return format(Decimal(repr(value)), "f")
def _mark_floats(node):
if isinstance(node, bool):
return node
if isinstance(node, float):
return f"{_FLOAT_TOKEN}{plain_float(node)}{_FLOAT_TOKEN}"
if isinstance(node, dict):
return {key: _mark_floats(value) for key, value in node.items()}
if isinstance(node, (list, tuple)):
return [_mark_floats(item) for item in node]
return node
def pretty_json(data, indent: int = 2) -> str:
text = json.dumps(
_mark_floats(data), indent=indent, ensure_ascii=False, default=str
)
return text.replace(f'"{_FLOAT_TOKEN}', "").replace(f'{_FLOAT_TOKEN}"', "")
def slugify(text: str) -> str:
text = text.lower().strip()
text = re.sub(r"[^a-z0-9-]", "-", text)