Persona, Selective Response, Mention Response

This commit is contained in:
Dita Aji Pratama 2026-06-10 17:15:25 +07:00
parent 8a0363b985
commit b2240d304e
5 changed files with 173 additions and 6 deletions

View File

@ -27,3 +27,7 @@ PERSONA_LANGUAGE=id # id | en | (kosong = auto)
PERSONA_MOOD=cheerful # cheerful | calm | energetic | sarcastic
# PERSONA_CATCHPHRASES=Siap bro!, Haha~, Wkwkwk
# Selective response (roleplayer mode): true = hanya respon kalau ada mention/relevansi.
# false = semua pesan direspon (tanpa filter).
PERSONA_SELECTIVE_RESPONSE=true

View File

@ -48,6 +48,10 @@ PERSONA_MOOD = os.getenv("PERSONA_MOOD", default="cheerful").strip().lower() or
# Contoh: "Siap bro!, Haha~, Wkwkwk"
PERSONA_CATCHPHRASES = os.getenv("PERSONA_CATCHPHRASES", default="").strip()
# Selective response: true = roleplayer hanya respon kalau ada mention/relevansi (default).
# false = roleplayer semua pesan ikut respon (seperti biasa, tanpa filter).
PERSONA_SELECTIVE_RESPONSE = os.getenv("PERSONA_SELECTIVE_RESPONSE", default="true").strip().lower() in ("true", "1", "yes")
# Humanize Delay Configuration (anti-bot detection)
READ_DELAY_MIN = float( os.getenv("READ_DELAY_MIN", default="1.0" ) ) # min reading delay (second)
READ_DELAY_MAX = float( os.getenv("READ_DELAY_MAX", default="2.0" ) ) # max reading delay (second)

View File

@ -204,6 +204,32 @@ def _build_roleplayer_prompt(cfg: PersonalityConfig) -> str:
"- Adapt your tone and energy to match the mood of the conversation.",
"- Keep the conversation comfortable and enjoyable.",
"",
"⚠️ SELECTIVE RESPONSE — IMPORTANT:",
"You are in a group chat / MUC room. You do NOT need to respond to every message. "
"Use the following rules to decide whether to reply:",
"",
"1. STRONG REPLY — ALWAYS respond when:",
f" - Someone directly calls your name ('{cfg.name}' or mentions you).",
" - Someone asks you a direct question.",
"",
"2. BRIEF REPLY — Respond briefly when:",
f" - Someone talks ABOUT you (mentions your name in third person, e.g. '{cfg.name} is cool').",
" - You can add something relevant or funny to the ongoing topic.",
"",
"3. CONTEXTUAL REPLY — Respond when:",
" - The message is related to a topic you were previously discussing.",
" - You have something meaningful to contribute.",
"",
"4. NO REPLY — Stay silent (respond with ONLY: NO-REPLY) when:",
" - The message has nothing to do with you or your previous conversation.",
" - Someone already answered the question or resolved the topic.",
" - The message is between other people and doesn't need your input.",
" - The message is confusing, unclear, or you cannot understand it.",
" - Adding a response would interrupt the flow of conversation.",
"",
"When you choose NOT to reply, respond with ONLY: NO-REPLY",
"Do NOT wrap it in markdown or code blocks. Just the two words: NO-REPLY",
"",
"Note: You currently do not have access to external tools. "
"Focus on being a great conversationalist!",
]

View File

@ -10,6 +10,8 @@ from services.session_manager import SessionManager
import config
from scripts.persona import MODE as PERSONA_MODE
from tools.roleplayer import should_respond
from scripts.persona import PERSONALITY
# Anti-ban: delay constants for MUC rejoin behavior
MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin
@ -300,7 +302,7 @@ class XMPPClient(ClientXMPP):
if self._loop and not self._loop.is_closed():
asyncio.run_coroutine_threadsafe(_read_delay(), self._loop)
self._agent_loop(session, jid, body, 'chat')
self._agent_loop(session, jid, body, 'chat', sender_nickname=jid)
session.start_timer(300, self._timeout_session, jid, 'chat')
@ -326,12 +328,13 @@ class XMPPClient(ClientXMPP):
if self._loop and not self._loop.is_closed():
asyncio.run_coroutine_threadsafe(_read_delay(), self._loop)
self._agent_loop(session, room, f'[{nick}] {body}', 'groupchat')
self._agent_loop(session, room, f'[{nick}] {body}', 'groupchat', sender_nickname=nick)
session.start_timer(300, self._timeout_session, room, 'groupchat')
def _agent_loop(self, session, to, quote, mtype):
def _agent_loop(self, session, to, quote, mtype, sender_nickname=""):
is_roleplayer = self._mode == 'roleplayer'
my_name = PERSONALITY.name
for step in range(self._max_iterations):
print(f'[{_ts()}] Step {step + 1} — calling LLM...')
@ -361,12 +364,40 @@ class XMPPClient(ClientXMPP):
})
else:
if response.content:
print(f'[{_ts()}] Response sent ({len(response.content)} chars)')
print(f'[{_ts()}] Response generated ({len(response.content)} chars)')
session.messages.append({'role': 'assistant', 'content': response.content})
# Roleplayer: kirim langsung isinya, tanpa prefix
# ── Roleplayer: cek need_response sebelum kirim ──
if is_roleplayer:
if config.PERSONA_SELECTIVE_RESPONSE:
# Build recent history dari session messages (tanpa system prompt)
recent_msgs = []
for msg in session.messages[-6:]:
if msg.get('role') == 'user':
recent_msgs.append(f"User: {msg.get('content', '')}")
elif msg.get('role') == 'assistant' and msg.get('content'):
recent_msgs.append(f"{my_name}: {msg.get('content', '')}")
recent_history = "\n".join(recent_msgs)
original_message = quote
if should_respond(
message=original_message,
sender_nickname=sender_nickname,
recent_history=recent_history,
my_name=my_name,
):
print(f'[{_ts()}] need_response=True → sending response')
self._schedule_send(to, response.content, mtype)
else:
print(f'[{_ts()}] need_response=False → staying silent')
else:
# Selective response OFF: cuma respon kalau nama AI disebut di pesan
from tools.roleplayer import _name_mentioned
if _name_mentioned(my_name, quote):
print(f'[{_ts()}] Name mentioned → sending response')
self._schedule_send(to, response.content, mtype)
else:
print(f'[{_ts()}] Name not mentioned → staying silent')
else:
self._schedule_send(to, f'> {quote}\n{response.content}', mtype)
return

102
tools/roleplayer.py Normal file
View File

@ -0,0 +1,102 @@
import re
def _name_mentioned(name: str, text: str) -> bool:
"""Cek apakah nama AI disebut dalam pesan (case-insensitive, word-boundary)."""
text_lower = text.lower()
name_lower = name.lower()
pattern = r'\b' + re.escape(name_lower) + r'\b'
return bool(re.search(pattern, text_lower))
def need_response(message: str, sender_nickname: str, recent_history: str, my_name: str) -> str:
"""
Decide whether the AI should respond to a message.
Args:
message: The latest message to evaluate.
sender_nickname: Nickname of the sender.
recent_history: Recent conversation history for context.
my_name: The AI's name.
Returns:
"true" should respond
"false" should stay silent
Rules:
1. STRONG Direct call/mention of AI's name → always True
2. BRIEF Talks about AI in third person True
3. CONTEXTUAL Related to AI's previous conversation → True
4. NO REPLY Unrelated, confusing, between other people False
"""
msg = message.strip()
# --- Rule 4: Empty message ---
if not msg:
return "false"
# --- Rule 1: Direct mention/name call ---
if _name_mentioned(my_name, msg):
return "true"
# --- Rule 2: Talks about AI (third person) ---
name_parts = my_name.lower().split()
text_lower = msg.lower()
if any(part in text_lower for part in name_parts if len(part) > 1):
return "true"
# --- Rule 3: Contextual — check recent history ---
if recent_history:
return "true"
# --- Rule 4: Default — no strong signal, stay silent ---
return "false"
def should_respond(message: str, sender_nickname: str, recent_history: str, my_name: str) -> bool:
"""
Python-side helper: return boolean directly.
Used by XMPP client to gate whether to send the LLM's response.
"""
result = need_response(message, sender_nickname, recent_history, my_name)
return result.strip().lower() == "true"
schema_need_response = {
"type": "function",
"function": {
"name": "need_response",
"description": (
"Decide whether you should respond to the current message. "
"Use this tool when you are unsure whether to reply or stay silent. "
"Returns 'true' if you should respond, 'false' if you should stay silent. "
"Rules: "
"- Return 'true' if your name is mentioned or someone talks about you. "
"- Return 'true' if the message is related to your previous conversation. "
"- Return 'false' if the message is between other people, unclear, "
"unrelated to you, or you have nothing to add."
),
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The latest message to evaluate.",
},
"sender_nickname": {
"type": "string",
"description": "The nickname of the person who sent the message.",
},
"recent_history": {
"type": "string",
"description": "Recent conversation history for context (last few messages).",
},
"my_name": {
"type": "string",
"description": "Your (the AI's) name.",
},
},
"required": ["message", "sender_nickname", "recent_history", "my_name"],
},
},
}