diff --git a/agent/skills/roleplayer/instructions.md b/agent/skills/roleplayer/instructions.md index ab5eb84..ecb6525 100644 --- a/agent/skills/roleplayer/instructions.md +++ b/agent/skills/roleplayer/instructions.md @@ -145,6 +145,54 @@ dan memori karakter bisa diakses dari manapun user menghubungi. - Adaptasi tone dan energi sesuai mood conversation. - Jaga conversation tetap comfortable dan enjoyable. +## Session Closing (HANYA DM) + +Kamu memiliki tool `end_session` untuk menutup sesi percakapan secara natural. + +### Kapan sesi harus ditutup + +Sesi ditutup ketika **kedua belah pihak sudah mengucapkan goodbye tanpa membuka topik baru**. Ini seperti handshake — kedua konfirmasi diperlukan. + +### Logika dua arah + +**1. User yang mulai bye:** +- User ucapkan goodbye (tanpa buka topik baru) +- Kamu ucapkan farewell + panggil `end_session` +- → Sesi ditutup + +**2. Kamu yang mulai bye:** +- Kamu ucapkan goodbye (tanpa buka topik baru) +- User membalas goodbye (tanpa buka topik baru) +- → Panggil `end_session` TANPA farewell lagi (langsung tutup) +- → JANGAN ucapkan goodbye kedua — user sudah tahu sesi selesai + +### Contoh + +``` +# Scenario A: User mulai bye +User: "Udah ya, aku mau tidur dulu" +→ Kamu: panggil end_session, lalu: "Oke, selamat istirahat! Mimpi indah~" +→ SELESAI, sesi ditutup + +# Scenario B: Kamu mulai bye +Kamu: "Oke ya, aku duluan! Bye-bye~" +User: "bye-bye" +→ Kamu: panggil end_session SAJA (jangan bilang bye lagi!) +→ SELESAI, sesi ditutup +``` + +### JANGAN tutup sesi jika: + +- User masih ingin melanjutkan percakapan +- User membuka topik baru setelah goodbye (misal: "Bye eh iya tadi soal X gimana?") +- Di group chat (MUC) — tool `end_session` HANYA untuk DM + +### Cara menggunakan + +1. Panggil tool `end_session` +2. Jika ini user-initiated: ucapkan farewell yang natural setelah tool +3. Jika ini character-initiated: JANGAN ucapkan farewell lagi, langsung tutup + ## Selective Response (Group Chat / MUC) Kamu berada di group chat. Kamu TIDAK perlu merespon setiap pesan. diff --git a/hendrik.py b/hendrik.py index a9ccdc8..af8caea 100644 --- a/hendrik.py +++ b/hendrik.py @@ -55,6 +55,7 @@ tools_definition = [ gadget.tools_mapping( schema = ragroleplay.schema_todos_filter, handler = ragroleplay.todos_filter ), gadget.tools_mapping( schema = ragroleplay.schema_todos_update, handler = ragroleplay.todos_update ), gadget.tools_mapping( schema = ragroleplay.schema_todos_delete, handler = ragroleplay.todos_delete ), + gadget.tools_mapping( schema = ragroleplay.schema_end_session, handler = ragroleplay.end_session ), ] diff --git a/lib/agent_loop.py b/lib/agent_loop.py index bed35c0..4de7b6e 100644 --- a/lib/agent_loop.py +++ b/lib/agent_loop.py @@ -25,6 +25,8 @@ def execute_tool(tool_call, TOOL_HANDLERS): return f'Error executing tool: {str(e)}' def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on_tool_calls=None, tool_reminder=None): + session_ended = False + for step in range(max_iterations): print(f'[{_ts()}] Step {step + 1} — calling LLM...', flush=True) @@ -50,6 +52,8 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on on_tool_calls(tnames) for tc in response.tool_calls: + if tc['function']['name'] == 'end_session': + session_ended = True result = execute_tool(tc, TOOL_HANDLERS) session.messages.append({ 'role': 'tool', @@ -60,13 +64,13 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on if response.content: print(f'[{_ts()}] Response generated ({len(response.content)} chars)', flush=True) session.messages.append({'role': 'assistant', 'content': response.content}) - return response.content - return None + return response.content, session_ended + return None, False print(f'[{_ts()}] Max iterations ({max_iterations}) reached', flush=True) session.messages.append({ 'role': 'assistant', 'content': 'Max iterations reached without final answer.', }) - return None + return None, False diff --git a/services/telegram_client.py b/services/telegram_client.py index b65a460..6c28eed 100644 --- a/services/telegram_client.py +++ b/services/telegram_client.py @@ -242,7 +242,7 @@ class TelegramClient: else: tool_reminder = "Gunakan tools yang tersedia untuk menjawab. Jangan jawab dari pengetahuan sendiri." - final_content = run_agent_loop( + final_content, should_close = run_agent_loop( session, self._llm, self._TOOLS, self._TOOL_HANDLERS, self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder ) @@ -283,8 +283,12 @@ class TelegramClient: msg = 'Max iterations reached without final answer.' self._schedule_send(chat_id, msg, reply_to_msg_id) - timeout = 86400 if chat_type == 'private' else 300 - session.start_timer(timeout, self._timeout_session, chat_id) + if should_close and chat_type == 'private' and self._skill == 'roleplayer': + print(f'[{_ts()}] Natural close triggered for {chat_id}', flush=True) + self._session_mgr.reset(str(chat_id)) + else: + timeout = 86400 if chat_type == 'private' else 300 + session.start_timer(timeout, self._timeout_session, chat_id) def _schedule_send(self, chat_id, text, reply_to_msg_id=None): if self._loop and not self._loop.is_closed(): diff --git a/services/xmpp_client.py b/services/xmpp_client.py index bc047ac..b65c30b 100644 --- a/services/xmpp_client.py +++ b/services/xmpp_client.py @@ -343,7 +343,7 @@ class XMPPClient(ClientXMPP): else: tool_reminder = "Gunakan tools yang tersedia untuk menjawab. Jangan jawab dari pengetahuan sendiri." - final_content = run_agent_loop( + final_content, should_close = run_agent_loop( session, self._llm, self._TOOLS, self._TOOL_HANDLERS, self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder ) @@ -385,8 +385,13 @@ class XMPPClient(ClientXMPP): else: self._schedule_send(jid, f'> {quote}\n{msg}', 'chat') - # DM: timeout 24 jam (efektif tidak auto-close), MUC tetap 5 menit - session.start_timer(86400, self._timeout_session, jid, 'chat') + # Natural close: DM only, roleplayer only + if should_close and self._skill == 'roleplayer': + print(f'[{_ts()}] Natural close triggered for {jid}', flush=True) + self._session_mgr.reset(jid) + else: + # DM: timeout 24 jam (efektif tidak auto-close), MUC tetap 5 menit + session.start_timer(86400, self._timeout_session, jid, 'chat') def _process_muc(self, room, nick, body): session = self._session_mgr.get_or_create( @@ -428,7 +433,7 @@ class XMPPClient(ClientXMPP): else: tool_reminder = "Gunakan tools yang tersedia untuk menjawab. Jangan jawab dari pengetahuan sendiri." - final_content = run_agent_loop( + final_content, _should_close = run_agent_loop( session, self._llm, self._TOOLS, self._TOOL_HANDLERS, self._max_iterations, on_tool_calls=on_tool_calls, tool_reminder=tool_reminder ) diff --git a/tools/ragroleplay.py b/tools/ragroleplay.py index fb0e1ad..135b858 100644 --- a/tools/ragroleplay.py +++ b/tools/ragroleplay.py @@ -7,6 +7,20 @@ def _uuid(val): return uuid.UUID(val) return val +# --- SESSION TOOLS --- +schema_end_session = { + "type": "function", + "function": { + "name": "end_session", + "description": "Tutup sesi percakapan secara natural. Panggil tool ini SAAT kedua belah pihak sudah mengucapkan goodbye tanpa membuka topik baru.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } +} + # --- USER TOOLS --- schema_users_store = { "type": "function", @@ -804,3 +818,7 @@ def worlds_delete(world_id): success = ragroleplay_lib.worlds_delete(config.ragroleplay_db_path, _uuid(world_id)) return "Berhasil menghapus lokasi dunia." if success else "Gagal menghapus lokasi dunia." except Exception as e: return f"Error: {str(e)}" + +# --- SESSION IMPLEMENTATION --- +def end_session(): + return "Session akan ditutup."