hendrik/tools/roleplayer.py

161 lines
5.8 KiB
Python

import re
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"],
},
},
}
def _name_mentioned(name: str, text: str) -> bool:
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 _bot_nick_mentioned(name: str, text: str) -> bool:
"""Mention nama bot, termasuk varian suffix angka dari logika anti-ban
MUC (mis. 'lily' juga cocok untuk 'lily_1')."""
text_lower = text.lower()
pattern = r'\b' + re.escape(name.lower()) + r'(?:_\d+)?\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 ---
# Logic updated: We no longer return "true" just because history exists.
# The actual decision for context should be handled by the LLM via the tool call
# since determining 'relatedness' requires semantic understanding.
# However, as a Python helper, we return "false" to avoid false-positives,
# forcing the system to rely on the LLM's reasoning if called as a tool.
return "false"
# Pola sapaan XMPP MUC: pesan diawali "Nama: " (mis. "Tinny: O iya, ...").
# Guard (?!//) agar URL seperti "https://..." tidak dianggap sapaan.
_ADDRESS_PREFIX_RE = re.compile(r'^\s*([A-Za-z0-9_\-]+)\s*:(?!//)\s+\S')
def _collect_bot_names(my_name: str, bot_aliases=None) -> set:
"""Semua variasi nama bot: codename, kata penyusunnya, plus alias tambahan
(mis. nick MUC beserta varian suffix-nya seperti lily_1)."""
names = set()
def _add(value: str):
v = (value or '').strip().lower()
if not v:
return
names.add(v)
for part in v.split():
if len(part) > 1:
names.add(part)
_add(my_name)
for alias in (bot_aliases or []):
_add(alias)
return names
def should_respond(message: str, sender_nickname: str, recent_history: str, my_name: str,
bot_aliases=None) -> bool:
"""
Gate deterministik (tanpa LLM): apakah bot harus membalas pesan group/MUC.
Urutan aturan:
0. Pesan kosong → False
1. Pesan diawali "Nama: " (format sapaan XMPP MUC):
- Nama = nama bot → True (disapa langsung)
- Nama lain → False (jelas sedang bicara ke orang lain)
2. Nama bot muncul di mana pun dalam pesan (word-boundary) → True
3. Selain itu → False (default diam)
Catatan: `recent_history` dipertahankan demi kompatibilitas pemanggil
lama, tapi sengaja TIDAK dipakai — riwayat bukan alasan untuk membalas.
"""
msg = (message or '').strip()
if not msg:
return False
names = _collect_bot_names(my_name, bot_aliases)
# --- Aturan 1: format sapaan "Nama: pesan" ---
match = _ADDRESS_PREFIX_RE.match(msg)
if match:
target = match.group(1).lower()
# Cocokkan persis, atau varian suffix dari alias (mis. lily_1 vs lily)
for n in names:
if target == n or target.startswith(n + '_'):
return True
return False
# --- Aturan 2: mention nama bot (kata utuh, bukan substring) ---
return any(_bot_nick_mentioned(n, msg) for n in names)