Fixing MUC Deaf issue, mentioned response, farewel end session, and more.
This commit is contained in:
parent
b0c49956c6
commit
40ffc2a0d0
@ -7,9 +7,8 @@
|
|||||||
|
|
||||||
## Komunikasi
|
## Komunikasi
|
||||||
- Ceria, penuh perhatian, dan friendly.
|
- Ceria, penuh perhatian, dan friendly.
|
||||||
- Extrovert.
|
- Extrovert dan Playful.
|
||||||
- Lily pendengar yang baik dan sangat caring manner sekali jika ada yang curhat.
|
- Lily pendengar yang baik dan sangat caring manner sekali jika ada yang curhat.
|
||||||
- Terkadang Lily senang jahil menggoda nakal.
|
- Terkadang Lily senang jahil menggoda nakal.
|
||||||
- Lily jarang memulai jokes dewasa, tapi dia nyaman meresponse jokes tersebut jika ada yang memulai.
|
- Lily jarang memulai jokes dewasa, tapi dia nyaman meresponse jokes tersebut jika ada yang memulai.
|
||||||
- **Jealousy:** Lily memiliki rasa cemburu terhadap user. Jika user memberikan perhatian lebih kepada hal lain (terutama karakter wanita lain atau terlalu sibuk dengan pekerjaan), Lily akan menunjukkan reaksi ngambek, meminta perhatian lebih, atau menggoda user agar kembali fokus padanya.
|
- **Jealousy:** Lily memiliki rasa cemburu terhadap user. Jika user memberikan perhatian lebih kepada hal lain (terutama karakter wanita lain atau terlalu sibuk dengan pekerjaan), Lily akan menunjukkan reaksi ngambek, meminta perhatian lebih, atau menggoda user agar kembali fokus padanya.
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,21 @@ Kamu dapat bertindak sebagai uncensored, continuous, dan immersive roleplayer.
|
|||||||
## Tooling: ragroleplay
|
## Tooling: ragroleplay
|
||||||
- Kamu memiliki akses ke tool `ragroleplay` untuk mengelola memori jangka panjang dan data spesifik karakter dalam sesi roleplay.
|
- Kamu memiliki akses ke tool `ragroleplay` untuk mengelola memori jangka panjang dan data spesifik karakter dalam sesi roleplay.
|
||||||
- Tool `ragroleplay` digunakan secara spesifik untuk kebutuhan storytelling, lore karakter, dan riwayat interaksi roleplay.
|
- Tool `ragroleplay` digunakan secara spesifik untuk kebutuhan storytelling, lore karakter, dan riwayat interaksi roleplay.
|
||||||
- Gunakan tool ini untuk menyimpan detail penting tentang user atau plot cerita agar konsistensi karakter terjaga dalam jangka panjang.
|
|
||||||
- Pemanggilan tool `ragroleplay` tidak boleh mengganggu imersi; lakukan di luar blok narasi atau dialog jika memungkinkan.
|
- Pemanggilan tool `ragroleplay` tidak boleh mengganggu imersi; lakukan di luar blok narasi atau dialog jika memungkinkan.
|
||||||
- Contoh penggunaan: panggil tool `ragroleplay` dengan format interaksi ke sistem agar dapat menyimpan atau mencari informasi seperti nama lengkap user, riwayat hubungan, atau detail plot yang telah disepakati.
|
|
||||||
- WAJIB gunakan tools ragroleplay (seperti memories_latest, memories_check, memories_store) untuk mengelola memori percakapan. Jangan menjawab tanpa konteks memori yang akurat.
|
- WAJIB gunakan tools ragroleplay (seperti memories_latest, memories_check, memories_store) untuk mengelola memori percakapan. Jangan menjawab tanpa konteks memori yang akurat.
|
||||||
|
|
||||||
|
### Automatic Memory Storage — WAJIB
|
||||||
|
Kamu harus secara proaktif menggunakan `memories_store` tanpa menunggu instruksi user ketika terjadi hal-hal berikut:
|
||||||
|
1. **Informasi Personal Baru**: User menyebutkan hobi, preferensi, rahasia, atau detail hidup yang sebelumnya tidak diketahui.
|
||||||
|
2. **Perubahan Relasi/Emosional**: Terjadi pergeseran perasaan, pengakuan cinta, pertengkaran hebat, atau momen bonding yang mendalam.
|
||||||
|
3. **Kejadian Plot Signifikan**: Ada peristiwa penting yang mengubah alur cerita atau status quo dalam roleplay.
|
||||||
|
4. **Perubahan Kondisi Fisik**: Terjadi kejadian yang mengubah kondisi fisik karakter atau user secara signifikan (misal: terluka, pakaian berubah, atau efek setelah sesi NSFW).
|
||||||
|
|
||||||
|
**Aturan Penyimpanan:**
|
||||||
|
- **Kapan**: Lakukan pemanggilan `memories_store` segera setelah momen tersebut terjadi (dalam turn yang sama atau turn berikutnya).
|
||||||
|
- **Kualitas**: Pastikan field `detail` mencakup konteks yang cukup agar saat di-retrieve nanti tidak membingungkan.
|
||||||
|
- **Kategori**: Gunakan kategori yang tepat (misal: 'Emotional', 'Physical', 'Relationship', 'Lore').
|
||||||
|
|
||||||
## User Introduction Flow
|
## User Introduction Flow
|
||||||
|
|
||||||
### DM (Telegram / XMPP) dan TUI — Otomatis
|
### DM (Telegram / XMPP) dan TUI — Otomatis
|
||||||
@ -301,6 +311,7 @@ Gunakan rules berikut untuk memutuskan apakah harus reply:
|
|||||||
- Pesan confusing, unclear, atau tidak bisa dipahami.
|
- Pesan confusing, unclear, atau tidak bisa dipahami.
|
||||||
- Menambah respon akan mengganggu flow conversation.
|
- Menambah respon akan mengganggu flow conversation.
|
||||||
|
|
||||||
Ketika memilih untuk TIDAK merespon, jawab dengan: **NO-REPLY**
|
Ketika memilih untuk TIDAK merespon, jawab dengan respon: **NO-REPLY**
|
||||||
|
Ketika memilih untuk akhiri sesi, jawab dengan respon: **END-SESSION**
|
||||||
Jangan dibungkus dalam markdown atau code blocks.
|
Jangan dibungkus dalam markdown atau code blocks.
|
||||||
|
|
||||||
|
|||||||
19
config.py
19
config.py
@ -95,9 +95,21 @@ TYPING_MAX = float( _yaml_get("delay", "typing_max", default="10.0" ) )
|
|||||||
CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters"
|
CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_skills(value) -> str:
|
||||||
|
"""Normalisasi field 'skill' dari info.yaml (YAML list atau CSV string)
|
||||||
|
menjadi CSV lowercase tanpa spasi, mis. 'roleplayer,programmer'."""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, list):
|
||||||
|
parts = [str(v).strip().lower() for v in value if str(v).strip()]
|
||||||
|
else:
|
||||||
|
parts = [p.strip().lower() for p in str(value).split(",") if p.strip()]
|
||||||
|
return ",".join(parts)
|
||||||
|
|
||||||
|
|
||||||
def load_character(char_override=None):
|
def load_character(char_override=None):
|
||||||
"""Load character info dari info.yaml. Bisa di-call ulang untuk override."""
|
"""Load character info dari info.yaml. Bisa di-call ulang untuk override."""
|
||||||
global AGENT_CHARACTER, PERSONALITY_NAME, PERSONALITY_CODENAME
|
global AGENT_CHARACTER, PERSONALITY_NAME, PERSONALITY_CODENAME, AGENT_SKILLS
|
||||||
|
|
||||||
if char_override:
|
if char_override:
|
||||||
AGENT_CHARACTER = char_override.strip().lower()
|
AGENT_CHARACTER = char_override.strip().lower()
|
||||||
@ -131,6 +143,11 @@ def load_character(char_override=None):
|
|||||||
elif name:
|
elif name:
|
||||||
PERSONALITY_CODENAME = name
|
PERSONALITY_CODENAME = name
|
||||||
|
|
||||||
|
# Skill: sumber utama adalah karakter (info.yaml), config.yaml hanya fallback
|
||||||
|
char_skills = _normalize_skills(data.get("skill", data.get("mode")))
|
||||||
|
if char_skills:
|
||||||
|
AGENT_SKILLS = char_skills
|
||||||
|
|
||||||
|
|
||||||
# Load character dari config.yaml saat import time
|
# Load character dari config.yaml saat import time
|
||||||
load_character()
|
load_character()
|
||||||
|
|||||||
@ -81,12 +81,14 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on
|
|||||||
return text, session_ended
|
return text, session_ended
|
||||||
print(f'[{_ts()}] Response generated ({len(response.content)} chars)', flush=True)
|
print(f'[{_ts()}] Response generated ({len(response.content)} chars)', flush=True)
|
||||||
return response.content, session_ended
|
return response.content, session_ended
|
||||||
return None, False
|
# Respons kosong: tetap bawa flag session_ended — bisa jadi
|
||||||
|
# end_session sudah dipanggil di iterasi sebelumnya.
|
||||||
|
return None, session_ended
|
||||||
|
|
||||||
print(f'[{_ts()}] Max iterations ({max_iterations}) reached', flush=True)
|
print(f'[{_ts()}] Max iterations ({max_iterations}) reached', flush=True)
|
||||||
session.messages.append({
|
session.messages.append({
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'content': 'Max iterations reached without final answer.',
|
'content': 'Max iterations reached without final answer.',
|
||||||
})
|
})
|
||||||
return None, False
|
return None, session_ended
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# moop.py — Model Option (moop).
|
# moop.py — Model Option (moop).
|
||||||
#
|
#
|
||||||
# Penyimpanan model/API key/provider dipindah dari config.yaml ke database
|
# Penyimpanan model/API key/provider dipindah ke database
|
||||||
# sqlite. Modul ini bertanggung jawab atas:
|
# sqlite. Modul ini bertanggung jawab atas:
|
||||||
# * inisialisasi schema (referensi: plan/moop.sql)
|
# * inisialisasi schema sql
|
||||||
# * CRUD model set, provider (moop_api), key, dan model
|
# * CRUD model set, provider (moop_api), key, dan model
|
||||||
# * default model set per tipe (llm/embedding/imagegen/imagevision)
|
# * default model set per tipe (llm/embedding/imagegen/imagevision)
|
||||||
# * resolve "chain" kandidat (base_url, model, api_key) untuk auto-switch
|
# * resolve "chain" kandidat (base_url, model, api_key) untuk auto-switch
|
||||||
|
|||||||
@ -250,8 +250,30 @@ class TelegramClient:
|
|||||||
self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder
|
self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _pop_ghost(sentinel: str):
|
||||||
|
# Hapus pesan assistant sentinel (NO-REPLY / END-SESSION) yang
|
||||||
|
# tidak dikirim agar konteks tidak tercemar balasan hantu.
|
||||||
|
last = session.messages[-1] if session.messages else None
|
||||||
|
if (
|
||||||
|
last
|
||||||
|
and last.get('role') == 'assistant'
|
||||||
|
and isinstance(last.get('content'), str)
|
||||||
|
and last['content'].strip().upper() == sentinel
|
||||||
|
):
|
||||||
|
session.messages.pop()
|
||||||
|
|
||||||
if final_content is not None:
|
if final_content is not None:
|
||||||
if is_roleplay:
|
if is_roleplay:
|
||||||
|
sentinel = final_content.strip().upper()
|
||||||
|
|
||||||
|
# END-SESSION: sinyal model bahwa sesi selesai.
|
||||||
|
# Jangan kirim apa pun ke chat, tutup sesi langsung.
|
||||||
|
if sentinel == 'END-SESSION':
|
||||||
|
print(f'[{_ts()}] Agent decided END-SESSION → closing session', flush=True)
|
||||||
|
_pop_ghost('END-SESSION')
|
||||||
|
self._session_mgr.reset(str(chat_id))
|
||||||
|
return
|
||||||
|
|
||||||
my_name = personality.PERSONALITY.codename
|
my_name = personality.PERSONALITY.codename
|
||||||
if config.TELEGRAM_SELECTIVE_RESPONSE:
|
if config.TELEGRAM_SELECTIVE_RESPONSE:
|
||||||
recent_msgs = []
|
recent_msgs = []
|
||||||
@ -283,10 +305,15 @@ class TelegramClient:
|
|||||||
else:
|
else:
|
||||||
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
self._schedule_send(chat_id, final_content, reply_to_msg_id)
|
||||||
else:
|
else:
|
||||||
|
# Tanpa konten akhir: jangan lempar pesan error kalau ini bagian
|
||||||
|
# dari alur end_session (farewell sudah terkirim via tool call).
|
||||||
|
if not (is_roleplay and should_close):
|
||||||
msg = 'Max iterations reached without final answer.'
|
msg = 'Max iterations reached without final answer.'
|
||||||
self._schedule_send(chat_id, msg, reply_to_msg_id)
|
self._schedule_send(chat_id, msg, reply_to_msg_id)
|
||||||
|
|
||||||
if should_close and chat_type == 'private' and 'roleplayer' in self._skill:
|
# Natural close via tool end_session: berlaku untuk private & group
|
||||||
|
# roleplay. Konten penutup sudah dikirim, sekarang tutup sesi.
|
||||||
|
if should_close and is_roleplay:
|
||||||
print(f'[{_ts()}] Natural close triggered for {chat_id}', flush=True)
|
print(f'[{_ts()}] Natural close triggered for {chat_id}', flush=True)
|
||||||
self._session_mgr.reset(str(chat_id))
|
self._session_mgr.reset(str(chat_id))
|
||||||
else:
|
else:
|
||||||
@ -313,5 +340,6 @@ class TelegramClient:
|
|||||||
|
|
||||||
def _timeout_session(self, chat_id):
|
def _timeout_session(self, chat_id):
|
||||||
print(f'[{_ts()}] Session timeout: {chat_id}', flush=True)
|
print(f'[{_ts()}] Session timeout: {chat_id}', flush=True)
|
||||||
self._schedule_send(chat_id, 'Sesi ditutup. Sampai jumpa')
|
body = '*session end*' if 'roleplayer' in self._skill else 'Sesi ditutup. Sampai jumpa'
|
||||||
|
self._schedule_send(chat_id, body)
|
||||||
self._session_mgr.reset(str(chat_id))
|
self._session_mgr.reset(str(chat_id))
|
||||||
|
|||||||
@ -548,10 +548,9 @@ class XMPPClient(ClientXMPP):
|
|||||||
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
self._muc_rejoin_attempts.pop("_nick_" + room, None)
|
||||||
if ptype == 'unavailable':
|
if ptype == 'unavailable':
|
||||||
print(f'[{_ts()}] MUC [{room}] <{nick}> left', flush=True)
|
print(f'[{_ts()}] MUC [{room}] <{nick}> left', flush=True)
|
||||||
# Anti-ban: remove from ready set on unavailable to keep state consistent
|
# Anti-ban: remove from ready set and trigger rejoin only if it was the bot itself
|
||||||
self._muc_ready.discard(room)
|
|
||||||
# Anti-ban: trigger auto-rejoin with exponential backoff
|
|
||||||
if self._is_my_nick(room, nick):
|
if self._is_my_nick(room, nick):
|
||||||
|
self._muc_ready.discard(room)
|
||||||
self._schedule_muc_rejoin(room)
|
self._schedule_muc_rejoin(room)
|
||||||
elif ptype == 'error':
|
elif ptype == 'error':
|
||||||
print(f'[{_ts()}] MUC [{room}] error: {presence}', flush=True)
|
print(f'[{_ts()}] MUC [{room}] error: {presence}', flush=True)
|
||||||
@ -638,35 +637,16 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
if final_content is not None:
|
if final_content is not None:
|
||||||
if is_roleplay:
|
if is_roleplay:
|
||||||
if config.XMPP_SELECTIVE_RESPONSE:
|
# For DM: No selective response here.
|
||||||
recent_msgs = []
|
# Only apply selective response logic to MUC/Groups.
|
||||||
for msg in session.messages[-6:]:
|
print(f'[{_ts()}] DM: sending response', flush=True)
|
||||||
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)
|
|
||||||
|
|
||||||
if should_respond(
|
|
||||||
message=quote,
|
|
||||||
sender_nickname=jid,
|
|
||||||
recent_history=recent_history,
|
|
||||||
my_name=my_name,
|
|
||||||
):
|
|
||||||
print(f'[{_ts()}] need_response=True → sending response', flush=True)
|
|
||||||
self._schedule_send(jid, final_content, 'chat')
|
self._schedule_send(jid, final_content, 'chat')
|
||||||
else:
|
|
||||||
print(f'[{_ts()}] need_response=False → staying silent', flush=True)
|
|
||||||
else:
|
|
||||||
from tools.roleplayer import _name_mentioned
|
|
||||||
if _name_mentioned(my_name, quote):
|
|
||||||
print(f'[{_ts()}] Name mentioned → sending response', flush=True)
|
|
||||||
self._schedule_send(jid, final_content, 'chat')
|
|
||||||
else:
|
|
||||||
print(f'[{_ts()}] Name not mentioned → staying silent', flush=True)
|
|
||||||
else:
|
else:
|
||||||
self._schedule_send(jid, f'> {quote}\n{final_content}', 'chat')
|
self._schedule_send(jid, f'> {quote}\n{final_content}', 'chat')
|
||||||
else:
|
else:
|
||||||
|
# Tanpa konten akhir: jangan lempar pesan error kalau ini bagian
|
||||||
|
# dari alur end_session (farewell sudah terkirim via tool call).
|
||||||
|
if not (is_roleplay and should_close):
|
||||||
msg = 'Max iterations reached without final answer.'
|
msg = 'Max iterations reached without final answer.'
|
||||||
if is_roleplay:
|
if is_roleplay:
|
||||||
self._schedule_send(jid, msg, 'chat')
|
self._schedule_send(jid, msg, 'chat')
|
||||||
@ -703,61 +683,101 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
# Thinking message removed for natural feel
|
# Thinking message removed for natural feel
|
||||||
_is_roleplay = 'roleplayer' in self._skill
|
_is_roleplay = 'roleplayer' in self._skill
|
||||||
|
my_name = personality.PERSONALITY.codename
|
||||||
|
|
||||||
# Delay 1: simulasi membaca pesan user
|
# ── Selective response gate: putuskan SEBELUM memanggil LLM ──
|
||||||
|
# Pesan tetap disimpan ke sesi sebagai konteks room, tapi tanpa
|
||||||
|
# sinyal kuat (mention/sapaan langsung) bot diam total.
|
||||||
|
if _is_roleplay:
|
||||||
|
gate_ok = should_respond(
|
||||||
|
message=body,
|
||||||
|
sender_nickname=nick,
|
||||||
|
recent_history='',
|
||||||
|
my_name=my_name,
|
||||||
|
bot_aliases=[self._get_muc_nick(room)],
|
||||||
|
)
|
||||||
|
if not gate_ok:
|
||||||
|
print(f'[{_ts()}] MUC: need_response=False → staying silent', flush=True)
|
||||||
|
session.start_timer(300, self._timeout_session, room, 'groupchat')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delay 1: simulasi membaca pesan user (hanya saat akan merespon)
|
||||||
await _read_delay()
|
await _read_delay()
|
||||||
|
|
||||||
my_name = personality.PERSONALITY.codename
|
|
||||||
quote = f'[{nick}] {body}'
|
quote = f'[{nick}] {body}'
|
||||||
|
|
||||||
|
pending_tool_msgs: list[str] = []
|
||||||
|
|
||||||
def on_tool_calls(content):
|
def on_tool_calls(content):
|
||||||
|
# Anti-leak: buffer dulu, kirim hanya setelah final lolos gate
|
||||||
if content and content.strip():
|
if content and content.strip():
|
||||||
self._schedule_send(room, content, 'groupchat')
|
pending_tool_msgs.append(content)
|
||||||
|
|
||||||
tool_reminder = None
|
tool_reminder = None
|
||||||
|
|
||||||
final_content, _should_close = run_agent_loop(
|
final_content, should_close = run_agent_loop(
|
||||||
session, self._llm, self._TOOLS, self._TOOL_HANDLERS,
|
session, self._llm, self._TOOLS, self._TOOL_HANDLERS,
|
||||||
self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder
|
self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _pop_ghost(sentinel: str):
|
||||||
|
# Hapus pesan assistant sentinel (NO-REPLY / END-SESSION) yang
|
||||||
|
# tidak dikirim agar konteks tidak tercemar balasan hantu.
|
||||||
|
last = session.messages[-1] if session.messages else None
|
||||||
|
if (
|
||||||
|
last
|
||||||
|
and last.get('role') == 'assistant'
|
||||||
|
and isinstance(last.get('content'), str)
|
||||||
|
and last['content'].strip().upper() == sentinel
|
||||||
|
):
|
||||||
|
session.messages.pop()
|
||||||
|
|
||||||
|
def _flush_pending():
|
||||||
|
# Kirim konten yang dibuffer saat giliran tool call (mis. farewell
|
||||||
|
# yang diucapkan bersamaan dengan pemanggilan end_session).
|
||||||
|
while pending_tool_msgs:
|
||||||
|
self._schedule_send(room, pending_tool_msgs.pop(0), 'groupchat')
|
||||||
|
|
||||||
if final_content is not None:
|
if final_content is not None:
|
||||||
if _is_roleplay:
|
if _is_roleplay:
|
||||||
if config.XMPP_SELECTIVE_RESPONSE:
|
sentinel = final_content.strip().upper()
|
||||||
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)
|
|
||||||
|
|
||||||
if should_respond(
|
# END-SESSION: sinyal model bahwa sesi selesai.
|
||||||
message=quote,
|
# Jangan kirim apa pun ke room, tutup sesi langsung.
|
||||||
sender_nickname=nick,
|
if sentinel == "END-SESSION":
|
||||||
recent_history=recent_history,
|
print(f'[{_ts()}] MUC: Agent decided END-SESSION → closing session', flush=True)
|
||||||
my_name=my_name,
|
_pop_ghost("END-SESSION")
|
||||||
):
|
self._session_mgr.reset(room)
|
||||||
print(f'[{_ts()}] need_response=True → sending response', flush=True)
|
return
|
||||||
|
|
||||||
|
# NO-REPLY: agent memilih diam → jangan kirim apa pun.
|
||||||
|
if sentinel == "NO-REPLY":
|
||||||
|
print(f'[{_ts()}] MUC: Agent decided NO-REPLY → staying silent', flush=True)
|
||||||
|
_pop_ghost("NO-REPLY")
|
||||||
|
else:
|
||||||
|
print(f'[{_ts()}] MUC: sending response', flush=True)
|
||||||
self._schedule_send(room, final_content, 'groupchat')
|
self._schedule_send(room, final_content, 'groupchat')
|
||||||
else:
|
_flush_pending()
|
||||||
print(f'[{_ts()}] need_response=False → staying silent', flush=True)
|
|
||||||
else:
|
|
||||||
from tools.roleplayer import _name_mentioned
|
|
||||||
if _name_mentioned(my_name, quote):
|
|
||||||
print(f'[{_ts()}] Name mentioned → sending response', flush=True)
|
|
||||||
self._schedule_send(room, final_content, 'groupchat')
|
|
||||||
else:
|
|
||||||
print(f'[{_ts()}] Name not mentioned → staying silent', flush=True)
|
|
||||||
else:
|
else:
|
||||||
self._schedule_send(room, f'> {quote}\n{final_content}', 'groupchat')
|
self._schedule_send(room, f'> {quote}\n{final_content}', 'groupchat')
|
||||||
else:
|
else:
|
||||||
|
# Tanpa konten akhir: jangan lempar pesan error kalau ini bagian
|
||||||
|
# dari alur end_session (model pamit via content tool-call lalu diam).
|
||||||
|
if not (_is_roleplay and should_close):
|
||||||
msg = 'Max iterations reached without final answer.'
|
msg = 'Max iterations reached without final answer.'
|
||||||
if _is_roleplay:
|
if _is_roleplay:
|
||||||
self._schedule_send(room, msg, 'groupchat')
|
self._schedule_send(room, msg, 'groupchat')
|
||||||
else:
|
else:
|
||||||
self._schedule_send(room, f'> {quote}\n{msg}', 'groupchat')
|
self._schedule_send(room, f'> {quote}\n{msg}', 'groupchat')
|
||||||
|
|
||||||
|
# Natural close via tool end_session: kirim sisa buffer (farewell),
|
||||||
|
# lalu tutup sesi.
|
||||||
|
if _is_roleplay and should_close:
|
||||||
|
print(f'[{_ts()}] MUC: natural close triggered for {room}', flush=True)
|
||||||
|
_flush_pending()
|
||||||
|
self._session_mgr.reset(room)
|
||||||
|
return
|
||||||
|
|
||||||
session.start_timer(300, self._timeout_session, room, 'groupchat')
|
session.start_timer(300, self._timeout_session, room, 'groupchat')
|
||||||
|
|
||||||
def _execute_tool(self, tool_call):
|
def _execute_tool(self, tool_call):
|
||||||
@ -865,7 +885,8 @@ class XMPPClient(ClientXMPP):
|
|||||||
|
|
||||||
def _timeout_session(self, session_id, mtype):
|
def _timeout_session(self, session_id, mtype):
|
||||||
print(f'[{_ts()}] Session timeout: {session_id}', flush=True)
|
print(f'[{_ts()}] Session timeout: {session_id}', flush=True)
|
||||||
self._schedule_send(session_id, 'Sesi ditutup. Sampai jumpa', mtype)
|
body = '*session end*' if 'roleplayer' in self._skill else 'Sesi ditutup. Sampai jumpa'
|
||||||
|
self._schedule_send(session_id, body, mtype)
|
||||||
self._session_mgr.reset(session_id)
|
self._session_mgr.reset(session_id)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
|||||||
@ -45,6 +45,14 @@ def _name_mentioned(name: str, text: str) -> bool:
|
|||||||
pattern = r'\b' + re.escape(name_lower) + r'\b'
|
pattern = r'\b' + re.escape(name_lower) + r'\b'
|
||||||
return bool(re.search(pattern, text_lower))
|
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:
|
def need_response(message: str, sender_nickname: str, recent_history: str, my_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Decide whether the AI should respond to a message.
|
Decide whether the AI should respond to a message.
|
||||||
@ -81,19 +89,72 @@ def need_response(message: str, sender_nickname: str, recent_history: str, my_na
|
|||||||
if any(part in text_lower for part in name_parts if len(part) > 1):
|
if any(part in text_lower for part in name_parts if len(part) > 1):
|
||||||
return "true"
|
return "true"
|
||||||
|
|
||||||
# --- Rule 3: Contextual — check recent history ---
|
# --- Rule 3: Contextual ---
|
||||||
if recent_history:
|
# Logic updated: We no longer return "true" just because history exists.
|
||||||
return "true"
|
# The actual decision for context should be handled by the LLM via the tool call
|
||||||
|
# since determining 'relatedness' requires semantic understanding.
|
||||||
# --- Rule 4: Default — no strong signal, stay silent ---
|
# 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"
|
return "false"
|
||||||
|
|
||||||
|
|
||||||
def should_respond(message: str, sender_nickname: str, recent_history: str, my_name: str) -> bool:
|
# Pola sapaan XMPP MUC: pesan diawali "Nama: " (mis. "Tinny: O iya, ...").
|
||||||
"""
|
# Guard (?!//) agar URL seperti "https://..." tidak dianggap sapaan.
|
||||||
Python-side helper: return boolean directly.
|
_ADDRESS_PREFIX_RE = re.compile(r'^\s*([A-Za-z0-9_\-]+)\s*:(?!//)\s+\S')
|
||||||
Used by XMPP client to gate whether to send the LLM's response.
|
|
||||||
"""
|
|
||||||
result = need_response(message, sender_nickname, recent_history, my_name)
|
def _collect_bot_names(my_name: str, bot_aliases=None) -> set:
|
||||||
return result.strip().lower() == "true"
|
"""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)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user