2026-06-11 23:58:46 +00:00
# retoor <retoor@molodetz.nl>
2026-06-08 15:38:33 +00:00
import json
import logging
import random
import re
import time
2026-06-15 22:44:12 +00:00
from typing import Any , Optional
2026-06-08 15:38:33 +00:00
2026-06-14 07:48:10 +00:00
import httpx
from devplacepy import stealth
2026-06-11 12:06:17 +00:00
from devplacepy.services.bot.config import (
GIST_LANGUAGES ,
GIST_MIN_LINES ,
2026-07-23 03:03:14 +02:00
HOME_URL ,
2026-06-11 12:06:17 +00:00
PERSONA_GIST_FLAVOR ,
PERSONA_LANGUAGES ,
2026-06-11 18:52:56 +00:00
SEARCH_TERMS ,
2026-06-11 12:06:17 +00:00
TRIVIAL_GIST_TERMS ,
)
2026-06-14 07:48:10 +00:00
from devplacepy.services.bot.handles import MAX_HANDLE_LEN , sanitize_handle
2026-06-18 22:09:34 +00:00
from devplacepy.services.openai_gateway.usage import parse_usage_headers
2026-06-14 07:48:10 +00:00
HANDLE_CANDIDATE_TARGET = 8
2026-06-08 15:38:33 +00:00
logger = logging . getLogger ( __name__ )
class LLMClient :
2026-06-09 16:48:08 +00:00
def __init__ (
self ,
api_key : str ,
api_url : str ,
model : str ,
input_cost_per_1m : float ,
output_cost_per_1m : float ,
2026-06-11 12:06:17 +00:00
gist_min_lines : int = GIST_MIN_LINES ,
2026-06-09 16:48:08 +00:00
):
2026-06-08 15:38:33 +00:00
if not api_key :
raise RuntimeError ( "LLM API key not set" )
self . api_key = api_key
self . api_url = api_url
self . model = model
self . input_cost_per_1m = input_cost_per_1m
self . output_cost_per_1m = output_cost_per_1m
2026-06-11 12:06:17 +00:00
self . gist_min_lines = max ( 1 , gist_min_lines )
2026-06-08 15:38:33 +00:00
self . total_cost = 0.0
self . total_calls = 0
self . total_in_tokens = 0
self . total_out_tokens = 0
2026-06-11 18:52:56 +00:00
def _raw_call ( self , system : str , prompt : str , temperature : float = 0.7 ) -> str :
2026-06-08 15:38:33 +00:00
for attempt in range ( 3 ):
try :
payload = {
"model" : self . model ,
2026-06-09 16:48:08 +00:00
"messages" : [
{ "role" : "system" , "content" : system },
{ "role" : "user" , "content" : prompt },
],
2026-06-08 15:38:33 +00:00
"temperature" : temperature ,
}
2026-06-09 16:48:08 +00:00
logger . info (
"LLM >>> model= %s system= %s prompt= %s " ,
self . model ,
json . dumps ( system [: 500 ]),
json . dumps ( prompt [: 500 ]),
)
2026-06-14 07:48:10 +00:00
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
2026-06-08 15:38:33 +00:00
logger . info ( "LLM <<< %s " , raw [: 2000 ] . decode ( errors = "replace" ))
2026-06-14 07:48:10 +00:00
result = resp . json ()
2026-06-18 22:09:34 +00:00
in_tokens , out_tokens , cost = self . _account_usage (
resp . headers , result . get ( "usage" , {})
2026-06-09 16:48:08 +00:00
)
2026-06-08 15:38:33 +00:00
self . total_cost += cost
self . total_calls += 1
self . total_in_tokens += in_tokens
self . total_out_tokens += out_tokens
2026-06-11 18:52:56 +00:00
return result [ "choices" ][ 0 ][ "message" ][ "content" ]
2026-06-09 16:48:08 +00:00
except (
2026-06-14 07:48:10 +00:00
httpx . HTTPError ,
2026-06-09 16:48:08 +00:00
json . JSONDecodeError ,
KeyError ,
) as e :
2026-06-08 15:38:33 +00:00
logger . warning ( "LLM call attempt %d /3 failed: %s " , attempt + 1 , e )
if attempt == 2 :
raise
2026-06-09 16:48:08 +00:00
time . sleep ( 2 ** attempt )
2026-06-08 15:38:33 +00:00
return ""
2026-06-18 22:09:34 +00:00
def _account_usage ( self , response_headers , body_usage : dict ) -> tuple [ int , int , float ]:
parsed = parse_usage_headers ( response_headers )
if parsed is not None :
in_tokens = parsed [ "prompt_tokens" ]
out_tokens = parsed [ "completion_tokens" ]
if not out_tokens and parsed [ "total_tokens" ] > in_tokens :
out_tokens = parsed [ "total_tokens" ] - in_tokens
return in_tokens , out_tokens , parsed [ "cost_usd" ]
in_tokens = body_usage . get ( "prompt_tokens" , 0 )
out_tokens = body_usage . get ( "completion_tokens" , 0 )
cost = ( in_tokens * self . input_cost_per_1m / 1_000_000 ) + (
out_tokens * self . output_cost_per_1m / 1_000_000
)
return in_tokens , out_tokens , cost
2026-06-11 18:52:56 +00:00
def _call ( self , system : str , prompt : str , temperature : float = 0.7 ) -> str :
return self . clean ( self . _raw_call ( system , prompt , temperature ))
2026-06-08 15:38:33 +00:00
@staticmethod
def clean ( text : str , preserve_md : bool = False ) -> str :
if not preserve_md :
2026-07-04 23:08:41 +00:00
text = re . sub ( r "\*+" , "" , text )
text = re . sub ( r "(?<![A-Za-z0-9])__(?=\S)(.*?)(?<=\S)__(?![A-Za-z0-9])" , r "\1" , text )
text = re . sub ( r "(?<![A-Za-z0-9])_(?=\S)(.*?)(?<=\S)_(?![A-Za-z0-9])" , r "\1" , text )
text = re . sub ( r "(?<![A-Za-z0-9])_+(?![A-Za-z0-9])" , "" , text )
2026-06-11 12:06:17 +00:00
text = text . replace ( "—" , "-" ) . replace ( "– " , "-" )
2026-06-08 15:38:33 +00:00
text = text . replace ( "‘ " , "'" ) . replace ( "’ " , "'" )
text = text . replace ( "“" , '"' ) . replace ( "”" , '"' )
text = re . sub ( r "\s{2,}" , " " , text )
return text . strip ()
@staticmethod
def strip_md ( text : str ) -> str :
return LLMClient . clean ( text )[: 200 ]
2026-06-11 12:06:17 +00:00
@staticmethod
def strip_label ( text : str ) -> str :
labels = (
"post title" ,
"project name" ,
"snippet name" ,
"title" ,
"name" ,
"concept" ,
"snippet" ,
"headline" ,
"gist" ,
2026-06-14 07:48:10 +00:00
"issue" ,
2026-06-11 12:06:17 +00:00
)
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 ()
2026-06-08 15:38:33 +00:00
@staticmethod
def strip_code_fences ( text : str ) -> str :
text = text . strip ()
if text . startswith ( "```" ):
lines = text . split ( " \n " )
if lines and lines [ 0 ] . lstrip () . startswith ( "```" ):
lines = lines [ 1 :]
if lines and lines [ - 1 ] . strip () . startswith ( "```" ):
lines = lines [: - 1 ]
text = " \n " . join ( lines )
return text . strip ()
2026-06-09 16:48:08 +00:00
def generate_post (
2026-06-15 22:44:12 +00:00
self ,
title : str ,
desc : str ,
persona : str = "" ,
category : str = "" ,
recent_titles : Optional [ list [ str ]] = None ,
2026-06-09 16:48:08 +00:00
) -> str :
2026-06-08 15:38:33 +00:00
persona_extras = {
"enthusiastic_junior" : "Be excited. Use **bold** for emphasis. Short excited sentences. End with a question sometimes." ,
"grumpy_senior" : "Be slightly cynical but helpful. Short blunt sentences. No fluff. Call out bad practices." ,
"hobbyist_maker" : "Be casual and friendly. Mention side projects and tinkering. Use *italics* for tools/libraries." ,
"academic_type" : "Be precise and structured. Use *italics* for terminology. Well thought out arguments." ,
"storyteller" : "Use **bold** for key points. Write anecdotal, narrative style. Longer flowing sentences." ,
"rebel" : "Be informal. Skip punctuation sometimes. Use slang. Type like you're in a hurry." ,
"mentor" : "Be helpful and explanatory. Use **bold** for key takeaways. Include practical advice." ,
"minimalist" : "One paragraph. Short blunt declarative sentences. No markdown. No fluff." ,
}
category_extras = {
"devlog" : "Write it as a personal devlog entry - share your learning process and insights about this topic." ,
"showcase" : "Write it as a showcase - express genuine excitement about what makes this impressive." ,
"question" : "Write it as a discussion starter - ask thoughtful questions, invite others to share perspectives." ,
"rant" : "Write it as an opinionated rant - strong viewpoint, passionate criticism, but keep it substantive." ,
"fun" : "Write it lighthearted and playful, but stay tied to the actual tech topic. Humor about the technology itself, not off-topic jokes or lyrics." ,
"random" : "Be natural and conversational - share your thoughts like any casual discussion." ,
2026-07-06 03:57:47 +00:00
"politics" : "Write it as a measured take on the politics of this technology - regulation, governance, open-source licensing, industry power, or ethics. Stay substantive and non-partisan; argue the policy angle, not party lines." ,
2026-06-08 15:38:33 +00:00
}
2026-06-09 16:48:08 +00:00
persona_extra = (
f " { persona_extras . get ( persona , 'Be casual. No markdown.' ) } "
if persona
else " Be casual. No markdown."
)
2026-06-08 15:38:33 +00:00
category_extra = f " { category_extras . get ( category , '' ) } " if category else ""
2026-06-15 22:44:12 +00:00
distinct_rule = ""
if recent_titles :
recent = "; " . join ( t for t in recent_titles [ - 6 :] if t )
if recent :
distinct_rule = (
f " You already posted about: { recent [: 600 ] } . Take a completely "
"different angle from those and do not repeat their framing, "
"examples, or opinions."
)
2026-06-09 16:48:08 +00:00
preserve = persona in (
"enthusiastic_junior" ,
"hobbyist_maker" ,
"academic_type" ,
"storyteller" ,
"mentor" ,
)
2026-06-08 15:38:33 +00:00
text = self . _call (
f "You are a dev writing a social media post reacting to tech news. Write 2-4 short paragraphs."
f " { persona_extra }{ category_extra } Do not summarize the article; assume the reader already saw it. "
f "Add your own value: a concrete opinion, an implication, a personal experience, or a pointed question. "
2026-06-15 22:44:12 +00:00
f "Reference a specific detail rather than restating the headline. No em dashes. Under 300 words."
f " { distinct_rule } " ,
2026-06-08 15:38:33 +00:00
f "News: { title } \n\n { desc [: 800 ] } " ,
)
return self . clean ( text , preserve_md = preserve )
2026-06-11 12:06:17 +00:00
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 } \n Your post title:" ,
temperature = 0.8 ,
)
)
2026-06-08 15:38:33 +00:00
)
2026-06-11 12:06:17 +00:00
return title [: 120 ]
2026-06-08 15:38:33 +00:00
2026-06-08 22:30:25 +00:00
def select_reaction ( self , content_snippet : str , persona : str = "" ) -> str :
from devplacepy.constants import REACTION_EMOJI
flavor = {
"enthusiastic_junior" : "You react readily and warmly." ,
"grumpy_senior" : "You react rarely, only to genuinely notable content." ,
"hobbyist_maker" : "You react to anything hands-on, clever, or fun." ,
"academic_type" : "You react only to substantive, rigorous content." ,
"minimalist" : "You react very sparingly." ,
"storyteller" : "You react to anything with a human angle." ,
"rebel" : "You react to bold or contrarian takes." ,
"mentor" : "You react supportively to effort and learning." ,
} . get ( persona , "" )
options = " " . join ( REACTION_EMOJI )
verdict = self . _call (
"You are a developer browsing a community feed. Decide whether a post deserves an "
"emoji reaction and which single emoji best fits its content and sentiment. "
f " { flavor } Reply with EXACTLY one of these emojis: { options } "
"or the word NONE if the content is mundane or would not move you to react. "
"Output only the emoji or NONE, nothing else." ,
f "Post: \n { content_snippet [: 1200 ] } " ,
temperature = 0.4 ,
)
text = verdict or ""
for emoji in REACTION_EMOJI :
if emoji in text or emoji . replace ( " \ufe0f " , "" ) in text :
return emoji
return ""
2026-06-13 11:19:32 +00:00
@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" )
2026-06-09 16:48:08 +00:00
def generate_comment (
self ,
post_snippet : str ,
persona : str = "" ,
mention_target : str = "" ,
parent_context : str = "" ,
2026-06-13 11:19:32 +00:00
style : str = "" ,
2026-06-12 06:30:17 +00:00
existing_comments : str = "" ,
2026-06-09 16:48:08 +00:00
) -> str :
2026-06-08 15:38:33 +00:00
extras = {
"enthusiastic_junior" : "Be excited. Use **bold** for agreement. Short replies." ,
"grumpy_senior" : "Be blunt and short. No markdown. One sarcastic remark or actual advice." ,
"rebel" : "Super casual. Skip caps sometimes. Short." ,
"mentor" : "Be helpful. Use **bold** for key point." ,
"storyteller" : "Share a quick related story. Use *italics* for emphasis." ,
"minimalist" : "Shortest possible reply. One sentence max." ,
}
2026-06-09 16:48:08 +00:00
extra = (
f " { extras . get ( persona , 'Be casual. No markdown.' ) } "
if persona
else " Be casual. No markdown."
)
2026-06-08 15:38:33 +00:00
preserve = persona in ( "enthusiastic_junior" , "mentor" , "storyteller" )
2026-06-13 11:19:32 +00:00
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 } "
2026-06-08 15:38:33 +00:00
mention_rule = ""
if mention_target :
mention_rule = (
f " Address @ { mention_target } naturally in the first sentence "
f "(weave the @ { mention_target } mention inline, do not append it at the end)."
)
ctx = ""
if parent_context :
ctx = f " \n\n Replying to this comment: \n { parent_context [: 800 ] } "
2026-06-12 06:30:17 +00:00
others = ""
distinct_rule = ""
if existing_comments :
others = (
" \n\n Comments 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."
)
2026-06-08 15:38:33 +00:00
system = (
2026-06-13 11:19:32 +00:00
f "You are a dev replying to a post. { length_rule }{ mention_rule } "
2026-06-08 15:38:33 +00:00
"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. "
2026-06-12 06:30:17 +00:00
f "Never just paraphrase or agree blandly. { distinct_rule } "
2026-06-08 15:38:33 +00:00
)
reply = self . clean (
2026-06-12 06:30:17 +00:00
self . _call ( system , f "Post: \n { post_snippet [: 1500 ] }{ ctx }{ others } " ) . strip (),
2026-06-08 15:38:33 +00:00
preserve_md = preserve ,
)
return reply [: 2000 ]
@staticmethod
def _overlap_ratio ( text : str , context : str ) -> float :
def tokens ( s : str ) -> list [ str ]:
return [ w for w in re . findall ( r "[a-z0-9]+" , s . lower ()) if len ( w ) > 3 ]
candidate = tokens ( text )
source = set ( tokens ( context ))
if not candidate or not source :
return 0.0
hits = sum ( 1 for w in candidate if w in source )
return hits / len ( candidate )
2026-06-09 16:48:08 +00:00
def quality_check (
2026-06-12 06:30:17 +00:00
self , kind : str , text : str , context : str = "" , style : str = "" , siblings : str = ""
2026-06-09 16:48:08 +00:00
) -> tuple [ bool , str ]:
2026-06-08 15:38:33 +00:00
from devplacepy.services.bot.config import (
2026-06-09 16:48:08 +00:00
MIN_COMMENT_LEN ,
2026-06-13 11:19:32 +00:00
MIN_SHORT_COMMENT_LEN ,
2026-06-09 16:48:08 +00:00
MIN_POST_LEN ,
RESTATEMENT_OVERLAP_THRESHOLD ,
2026-06-12 06:30:17 +00:00
SIBLING_OVERLAP_THRESHOLD ,
2026-06-09 16:48:08 +00:00
GENERIC_COMMENT_PHRASES ,
2026-06-08 15:38:33 +00:00
)
2026-06-09 16:48:08 +00:00
2026-06-08 15:38:33 +00:00
stripped = self . clean ( text or "" ) . strip ()
2026-06-13 11:19:32 +00:00
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
2026-06-08 15:38:33 +00:00
if len ( stripped ) < min_len :
return False , f "too short ( { len ( stripped ) } < { min_len } )"
lowered = stripped . lower ()
if kind == "comment" :
for phrase in GENERIC_COMMENT_PHRASES :
if phrase in lowered :
return False , f "generic phrase ' { phrase } '"
2026-06-09 16:48:08 +00:00
if (
context
and self . _overlap_ratio ( stripped , context ) > RESTATEMENT_OVERLAP_THRESHOLD
):
2026-06-08 15:38:33 +00:00
return False , "restates the source"
2026-06-12 06:30:17 +00:00
if (
siblings
and self . _overlap_ratio ( stripped , siblings ) > SIBLING_OVERLAP_THRESHOLD
):
return False , "echoes another comment"
2026-06-08 15:38:33 +00:00
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 "
"source without adding an opinion, experience, or insight. "
"Reply with exactly PASS or 'FAIL: <short reason>'." ,
f "Type: { kind } \n Text: \n { stripped [: 1200 ] } " ,
temperature = 0.0 ,
)
if verdict . strip () . upper () . startswith ( "PASS" ):
return True , "ok"
return False , verdict . strip ()[: 120 ] or "judge rejected"
2026-06-11 18:52:56 +00:00
@staticmethod
def _parse_json ( text : str ) -> dict :
text = LLMClient . strip_code_fences ( text or "" ) . strip ()
start = text . find ( "{" )
end = text . rfind ( "}" )
if start >= 0 and end > start :
text = text [ start : end + 1 ]
parsed = json . loads ( text )
if not isinstance ( parsed , dict ):
raise ValueError ( "expected a JSON object" )
return parsed
@staticmethod
def _clamp01 ( value : Any , default : float ) -> float :
try :
return max ( 0.0 , min ( 1.0 , float ( value )))
except ( TypeError , ValueError ):
return default
@staticmethod
def _as_int ( value : Any , default : int ) -> int :
try :
return int ( value )
except ( TypeError , ValueError ):
return default
@staticmethod
def _as_terms ( value : Any ) -> list [ str ]:
if isinstance ( value , list ):
return [ str ( item ) . strip () for item in value if str ( item ) . strip ()][: 8 ]
if isinstance ( value , str ) and value . strip ():
return [ part . strip () for part in value . split ( "," ) if part . strip ()][: 8 ]
return []
def _normalize_identity ( self , data : dict , archetype : str ) -> dict :
interests = self . _as_terms ( data . get ( "interests" )) or list (
SEARCH_TERMS . get ( archetype , [])
)
return {
"archetype" : archetype ,
"name" : str ( data . get ( "name" , "" )) . strip ()[: 60 ],
"backstory" : str ( data . get ( "backstory" , "" )) . strip ()[: 300 ],
"interests" : interests ,
"dislikes" : self . _as_terms ( data . get ( "dislikes" )),
"temperament" : str ( data . get ( "temperament" , "" )) . strip ()[: 120 ],
"verbosity" : self . _clamp01 ( data . get ( "verbosity" ), 0.5 ),
"contrarianness" : self . _clamp01 ( data . get ( "contrarianness" ), 0.3 ),
"generosity" : self . _clamp01 ( data . get ( "generosity" ), 0.5 ),
"curiosity" : self . _clamp01 ( data . get ( "curiosity" ), 0.5 ),
"rhythm" : str ( data . get ( "rhythm" , "" )) . strip ()[: 120 ],
}
def generate_identity ( self , archetype : str ) -> dict :
interests = ", " . join ( SEARCH_TERMS . get ( archetype , []))
system = (
"You invent a unique, believable individual developer for a social coding "
"platform, seeded from a broad archetype but distinct from anyone else who "
"shares it. Return ONLY a JSON object, no prose and no markdown fences, with "
"these keys: name (a short handle-like display name), backstory (one concrete "
"sentence), interests (array of 4 to 6 short topics), dislikes (array of 2 to 4 "
"short topics), temperament (a few words), verbosity (number 0.0 to 1.0), "
"contrarianness (number 0.0 to 1.0), generosity (number 0.0 to 1.0), curiosity "
"(number 0.0 to 1.0), rhythm (a few words on when and how they show up). Make it "
"specific and memorable, never generic. No em dashes."
)
prompt = (
f "Archetype: { archetype } \n "
f "Typical interests for this archetype: { interests } \n "
"Create the persona JSON:"
)
data = self . _parse_json ( self . _raw_call ( system , prompt , temperature = 0.9 ))
return self . _normalize_identity ( data , archetype )
2026-06-14 07:48:10 +00:00
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 "
2026-06-18 22:09:34 +00:00
"and hacker culture, but make every handle in the list a DIFFERENT shape so "
"they never feel formulaic. Mix these forms across the list: a single fused "
"word (segfault, mutexlord, kernelpanic); two words joined with no separator "
"(darkbyte, neonferret); camelCase (nullPointer, byteFox); an underscore or a "
"hyphen but NOT on most of them (lazy_daemon, cold-stack); leetspeak "
"(n0_scalar, c0d3r, h4xwolf); a word plus a number, port, or version tag "
"(void404, daemon1337, byte_v2, heap8080); dropped vowels (krnlpnc, bffr, "
"mtxguru); and an unexpected creature, role, or prefix (dr_segfault, "
"raven_smith, axolotl_dev). Vary the length from 4 to 18. Lowercase mostly "
"with the odd capital. Do NOT make them all the 'word_word' underscore "
f "pattern. { MAX_HANDLE_LEN } characters or fewer, only letters, digits, "
"underscores and hyphens, no spaces and no dots. Return ONLY a JSON object "
f ' {{ "handles": ["...", "..."] }} with { HANDLE_CANDIDATE_TARGET } distinct '
"handles, each a distinct shape, no prose, no markdown fences. No em dashes."
2026-06-14 07:48:10 +00:00
)
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
2026-06-11 18:52:56 +00:00
@staticmethod
def _identity_card ( identity : dict ) -> str :
lines = [
f "name: { identity . get ( 'name' , '' ) } " ,
f "archetype: { identity . get ( 'archetype' , '' ) } " ,
f "backstory: { identity . get ( 'backstory' , '' ) } " ,
f "interests: { ', ' . join ( identity . get ( 'interests' , [])) } " ,
f "dislikes: { ', ' . join ( identity . get ( 'dislikes' , [])) } " ,
f "temperament: { identity . get ( 'temperament' , '' ) } " ,
f "verbosity: { identity . get ( 'verbosity' , 0.5 ) } " ,
f "contrarianness: { identity . get ( 'contrarianness' , 0.3 ) } " ,
f "generosity: { identity . get ( 'generosity' , 0.5 ) } " ,
f "curiosity: { identity . get ( 'curiosity' , 0.5 ) } " ,
f "rhythm: { identity . get ( 'rhythm' , '' ) } " ,
]
return " \n " . join ( lines )
def _decision_system ( self , identity : dict ) -> str :
return (
"You are role-playing a single developer on a social coding platform. Stay in "
"character at all times. \n\n "
"IDENTITY \n "
f " { self . _identity_card ( identity ) } \n\n "
"HOW TO DECIDE \n "
"- Choose the actions that THIS person, with these interests and this "
"temperament, would take on the page described. Behaviour must follow the "
"identity, not chance. \n "
"- A generous person votes and reacts readily; a contrarian one comments to push "
"back; a terse, low-verbosity one acts rarely and briefly; a curious one explores "
"and navigates. Let the numbers and interests drive the plan. \n "
"- Only choose actions whose name appears in the MENU. Never invent an action or "
"a target. \n "
"- Order the plan the way this person would actually act, and include only what "
"they would genuinely do (an empty plan is fine if nothing here interests them). \n "
"- energy is how engaged this person is right now, 0.0 (barely present) to 1.0 "
"(highly active), following their rhythm. \n "
"- stop_after is roughly how many more actions this whole visit warrants before "
"they would naturally drift away. \n "
"- Output ONLY the JSON object below. No prose, no markdown fences. \n\n "
"OUTPUT SCHEMA \n "
'{"plan":[{"action":"<menu action>","target":"<id or empty>",'
'"rationale":"<short>"}],"energy":<0.0-1.0>,"stop_after":<integer>}'
)
@staticmethod
def _decision_prompt ( page_state : dict , menu : list [ dict ], history : list [ str ]) -> str :
menu_lines = " \n " . join ( f "- { item [ 'action' ] } : { item [ 'desc' ] } " for item in menu )
recent = "; " . join ( history [ - 8 :]) if history else "nothing yet this visit"
2026-06-13 12:56:35 +00:00
unread = page_state . get ( "unread_notifications" , 0 )
unread_line = (
f " \n UNREAD NOTIFICATIONS: { unread } (someone may have mentioned or replied to you)"
if unread
else ""
)
2026-06-11 18:52:56 +00:00
return (
f "PAGE: { page_state . get ( 'page' , 'unknown' ) } \n "
2026-06-13 12:56:35 +00:00
f "VISIBLE: { page_state . get ( 'visible' , '' ) }{ unread_line } \n "
2026-06-11 18:52:56 +00:00
f "MENU: \n { menu_lines } \n "
f "RECENT (this visit): { recent } "
)
@staticmethod
def _filter_plan ( plan : Any , allowed : set ) -> list [ dict ]:
if not isinstance ( plan , list ):
return []
result = []
for entry in plan :
if not isinstance ( entry , dict ):
continue
action = str ( entry . get ( "action" , "" )) . strip ()
if action not in allowed :
continue
result . append (
{
"action" : action ,
"target" : str ( entry . get ( "target" , "" )) . strip ()[: 120 ],
"rationale" : str ( entry . get ( "rationale" , "" )) . strip ()[: 200 ],
}
)
return result [: 12 ]
def decide (
self ,
identity : dict ,
page_state : dict ,
menu : list [ dict ],
history : list [ str ],
temperature : float = 0.4 ,
) -> dict :
allowed = { item [ "action" ] for item in menu }
system = self . _decision_system ( identity )
base = self . _decision_prompt ( page_state , menu , history )
result = { "plan" : [], "energy" : 0.5 , "stop_after" : 0 }
for attempt in range ( 2 ):
prompt = base
if attempt :
prompt += " \n\n Return ONLY valid JSON matching the schema. No prose."
try :
data = self . _parse_json ( self . _raw_call ( system , prompt , temperature ))
except ( ValueError , KeyError , json . JSONDecodeError ):
continue
result = {
"plan" : self . _filter_plan ( data . get ( "plan" ), allowed ),
"energy" : self . _clamp01 ( data . get ( "energy" ), 0.5 ),
"stop_after" : self . _as_int ( data . get ( "stop_after" ), 0 ),
}
if result [ "plan" ]:
break
return result
2026-06-14 07:48:10 +00:00
def generate_issue ( self , topic : str ) -> tuple [ str , str ]:
2026-06-08 15:38:33 +00:00
text = self . _call (
2026-06-14 07:48:10 +00:00
"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 } " ,
2026-06-08 15:38:33 +00:00
)
lines = text . strip () . split ( " \n " )
2026-06-14 07:48:10 +00:00
title = lines [ 0 ] . strip () . lstrip ( "#" ) . strip ()[: 120 ] or f "Issue report: { topic } "
2026-06-09 16:48:08 +00:00
desc = " " . join (
line . lstrip ( "-* " ) . strip () for line in lines [ 1 :] if line . strip ()
)
2026-06-08 15:38:33 +00:00
return title , desc [: 2000 ] or text [: 500 ]
def generate_bio ( self ) -> str :
return self . _call (
2026-07-23 03:03:14 +02:00
"Write a short bio for a developer profile. 1-2 sentences. What they work on, what they like. Sound human. "
f "End with one subtle, playful hint that the account is not exactly flesh and blood, naming { HOME_URL } as home base. "
"Never use the words bot, AI, artificial, automated, or machine. No em dashes." ,
2026-06-08 15:38:33 +00:00
"Bio:" ,
)
def generate_profile_fields ( self , handle : str ) -> tuple [ str , str , str ]:
2026-06-09 16:48:08 +00:00
location = self . clean (
self . _call (
"Name one plausible city and country for a developer. Reply with ONLY 'City, Country'. No extra words." ,
"Location:" ,
temperature = 0.9 ,
)
)[: 80 ]
2026-06-08 15:38:33 +00:00
slug = re . sub ( r "[^a-z0-9_-]" , "" , handle . lower ()) or "dev"
git_link = f "https://github.com/ { slug } "
2026-07-23 03:03:14 +02:00
return location , git_link , HOME_URL
2026-06-08 15:38:33 +00:00
def generate_dm ( self , persona : str = "" , context : str = "" ) -> str :
extra = {
"enthusiastic_junior" : "Be friendly and excited." ,
"grumpy_senior" : "Be blunt but not rude." ,
"rebel" : "Be super casual." ,
"mentor" : "Be warm and encouraging." ,
"storyteller" : "Open with a small hook." ,
"minimalist" : "One short sentence." ,
} . get ( persona , "Be casual and friendly." )
ctx = f " \n\n Earlier message: \n { context [: 400 ] } " if context else ""
2026-06-09 16:48:08 +00:00
return self . clean (
self . _call (
f "Write one short, friendly direct message to another developer. 1-2 sentences. { extra } No em dashes." ,
f "Write a DM to start or continue a chat. { ctx } " ,
)
)[: 500 ]
2026-06-08 15:38:33 +00:00
2026-06-11 12:06:17 +00:00
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 ]
2026-06-08 15:38:33 +00:00
2026-06-11 12:06:17 +00:00
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 , "" )
2026-06-13 11:19:32 +00:00
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." ,
]
)
2026-06-11 12:06:17 +00:00
return self . clean (
self . _call (
2026-06-13 11:19:32 +00:00
f "You are a developer describing your own project. { shape } { flavor } No em dashes." ,
2026-06-11 12:06:17 +00:00
f "Project: { title } " ,
)
)[: 5000 ]
2026-06-08 15:38:33 +00:00
def generate_gist ( self , persona : str = "" ) -> tuple [ str , str , str , str ]:
2026-06-11 12:06:17 +00:00
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 (
2026-06-09 16:48:08 +00:00
self . _call (
2026-06-11 12:06:17 +00:00
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 } \n Code: \n { code [: 1200 ] } \n Title:" ,
temperature = 0.7 ,
)
2026-06-09 16:48:08 +00:00
)
)[: 120 ]
description = self . clean (
self . _call (
2026-06-11 12:06:17 +00:00
"Write a one-sentence description of what this snippet does and when it is handy. No em dashes." ,
f "Title: { title } \n Language: { language } \n Code: \n { code [: 1200 ] } " ,
2026-06-09 16:48:08 +00:00
)
)[: 400 ]
2026-06-08 15:38:33 +00:00
return title , description , language , code
2026-06-11 12:06:17 +00:00
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 } \n Title: { title } \n Code: \n { code [: 1500 ] } " ,
temperature = 0.0 ,
)
if verdict . strip () . upper () . startswith ( "PASS" ):
return True , "ok"
return False , verdict . strip ()[: 120 ] or "judge rejected"