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)