forked from retoor/devplacepy
chore: add pretty_json utility and sibling comment awareness to bot realism mechanics
This commit is contained in:
@@ -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 ""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user